1

I've decided to write two methods, one providing general functionality, and one doing something more specific and narrow that can be done with the more complex method, though maybe with worse performance than a dedicated implementation.

As an example, say that I want to write the methods map and iterate, with the former going over every element in a list and projecting it with a function, and the latter just going over every element in the list.

map<T, S>(list : List<T>, f : T => S) : List<S>

iterate<T>(list : List<T>, f : T => void) : void

I can either write the more complex method first and then call it using specific parameters in the simpler method (if that makes sense in terms of performance etc), or else write the simpler method first and do extra things in the more complex one (if that's possible).

Which is the better option?

  • 1
    I may be missing something, because I don't see how this is even a question. If the "specific" function uses the "general" function, then obviously you need to white the "general" function first. – JacquesB Jul 30 '17 at 15:36
  • 2
    @JacquesB: You don't have to write the "general" function first. The "specific" function should be a one liner that you can just write down. Of course it doesn't work so you can't test it at this point, but you can absolutely write it first. That's how top down development works. – gnasher729 Jul 30 '17 at 17:14
  • 4
    This seems like too much planning for writing just 2 functions. If you wish to waste more time before actually writing these two functions, you can read this: Analysis paralysis – scriptin Jul 30 '17 at 19:30
2

Assuming the specific function depends on the general function, but not vice-versa:

Writing the general function first allows you to run and test it before you start writing the specific function. This allows you an iterative process where you verify the foundation is correct before building on top of it.

But if you write the specific function first, you cannot run or test it until you also have also written the general function it depends on. This introduces higher risk and wrong assumptions will be more costly because it takes longer time before you discover the issues.

So start with the general function.

1

To take a really silly example (even simpler than yours), there are actually four patterns you could follow:

Implemented separately

int AddNumbers(int a, int b)
{
    return a + b;
}

int AddNumbers(int a, int b, int c)
{
    return a + b + c;
}

Pros: No dependencies, no extra indirection means (very slight) performance improvement

Cons: More work to write. If you find a bug you have to fix it in two places.

Simple first

int AddNumbers(int a, int b)
{
    return a + b;
}

int AddNumbers(int a, int b, int c)
{
    return AddNumbers(a, b) + c;
}

Pros: If you find a bug you might be able to fix it in just one place

Cons: Slight performance hit calling the complex version. Changes to the simple version may have unintended side effect on complex version that could be missed with ordinary unit testing which would isolate away the simple version.

Complex first

int AddNumbers(int a, int b, int c)
{
    return a + b + c;
}

int AddNumbers(int a, int b)
{
    return AddNumbers(a, b, 0);
}

Or in some languages you could just write

int AddNumbers(int a, int b, int c = 0)
{
    return a + b + c;
}

Pros: One function is doing all the work, the simple function just substitutes default value. Testing the simple function is trivial.

Cons: Slight performance hit. Possible side effect by creating default parameter, e.g. if the complex version accepts complex instead of primitive types (you have to allocate the third parameter). But works great with nullable types.

Layers

int AddNumbers(int a, int b, int c)
{
    return AddNumbersInternal(new int[] {a, b, c});
}

int AddNumbers(int a, int b)
{
    return AddNumbersInternal(new int[] {a, b});
}

private int AddNumbersInternal(int[] args)
{
    int accumulator = 0;
    foreach (int n in args) accumulator += n;
    return accumulator;
}

Pros: Crystal clear what is going on. Bugs fixed in one place. Allows customization of each prototype without affecting core logic.

Cons: More work.

In my opinion

All of the above approaches have their place. If what you are doing is very simple, I would implement them separately, to be honest. if what you are doing is very complex, I would implement using the layered approach.

But you're asking about the two middle approaches. I hate them both! But I would lean toward "complex first" since all the work is essentially done in one place, even if you add more and more prototypes that do different things. With the "simple first" approach you are splitting logic into two functions which can be confusing, and you'll have to split it more and more as more and more complex functions are needed.

0

It doesn't matter which one you physically author first. What you want to do is establish (by way of design/architecture) the relationship between the two.

If you know statically, when you are writing code that invokes one of them, the difference between the general case and the specialized case, then you have a case for using different signatures, e.g. overloads in some languages. The overloads with fewer parameters are implemented by invoking the general one with all the parameters.

You can implement the overloads using a specialized copy of the general code optimized for that case, but treat this as optimization, in which this increases the complexity of the code (by way of duplication) and should be undertaken at such point as you know this extra complexity offers true value.

If you can't/don't know statically which to choose (e.g. when writing code that invokes one of these) then the differentiation is dynamic. In the dynamic situation, the implementation effectively tests the parameters at runtime, and the appropriate (specialized) case is used. One mechanism for testing parameter conditions dynamically and choosing the right specialization is class hierarchy (e.g. virtual methods); another is multi-method dispatch (offered directly in some languages, and obtainable as a pattern in other languages).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.