The Chimera Function
I’ve written before about a function that essentially does two different versions of the same thing. It’s sort of cute to think that we can pass a boolean into a function to control whether it uses variant A or variant B of the algorithm.
There can be milder forms of this too. Perhaps we pass in a boolean to ask the function to include/exclude a prefix on its output:
function outputPrice(price: number, showCurrency: boolean) { const prefix = showCurrency ? '$ ' : ''; const numeric = ...; return `${prefix}${numeric}`; }
When I look at the above, which for me is right on the cusp of whether adding a variant to a function’s behaviour, I’m in two minds about whether it’s right. And that’s ok. I think if it got more complex, we’d refactor it to something less complex. If it stays this simple, I think it’s easy to understand and use.
However there are patterns where a network of related functions end up supporting completely different sets of behaviour, controlled in unexpected ways. We should watch out for these, as they’re hard to understand, and frequently indicate a lack of understanding of more conventional techniques. I’m guessing that in many cases a simple strategy pattern or a small bit of functional programming would be better.
Here’s something similar to a thing I discovered in the real world:
function entryPoint(data: SomeData, how: string) { const interestingData = extractUsefulDataFrom(data); output(interestingData, how); } function output(interestingData: SomeData, how: string) { const sorted = sort(interestingData); return inDisplayFormat(sorted, how); } function inDisplayFormat(sorted: Sorted, how: string) { switch (how) { case JSON: ...; case PLAINTEXT: ...; case XML: ...; } }
I’ve fictionalised this and taken out implementation details to try to focus on what’s weird here.
Function 1 takes a how
. It passes it THROUGH function 2, and this how
ends up being used in a switch
statement by function 3, who then uses a completely different implementation for each version.
There are three better solutions to this. Before we cover them, consider the unit tests we’d need for function 3. They would feel like the unit tests you’d write for multiple DIFFERENT functions. So the idea that they somehow all apply to the same function is a clue that we’ve somehow co-located different implementations in the same place.
Three better ways:
- Use the principle that functions should return something – have functions 1 + 2 return the
sorted
value to the caller for the caller to choose which output function to call - With different output functions, have the caller pass in the function they wish to use for rendering, instead of an arbitrary value that indicates which function to choose
- Have an OO strategy pattern where the
how
is actually an object which can render things
These days, I’m less likely to use an OO strategy pattern for a single operation, where a functional equivalent is more precise. You might argue that any single operation strategy IS the same as using a function. That’s up to you.
What’s It All About?
We’re skirting around the Single Responsibility Principle here. A function that can render in three formats may appear to have a single responsibility “render”, but actually contains multiple independent implementations.
Though perhaps this is exactly what’s happening in my first example, the extra complexities of an ad-hoc selector pattern, plus passing the selector THROUGH other functions, were enough to make my eyes water when I saw the real-life version of this example.
From my notes, I can’t remember whether at the time I refactored this out of existence, or whether I just left it there. I can’t believe that I would have left it.
Published on Java Code Geeks with permission by Ashley Frieze, partner at our JCG program. See the original article here: The Chimera Function Opinions expressed by Java Code Geeks contributors are their own. |