There's no such thing as a null strategy!
Here is a simple refactoring/micro-pattern that can help remove a little complexity from classes that use passed-in behaviours/strategies/handlers/etc. 1
🤔 TODO: A more concrete example?
The problem: Checking strategies for nullness
Below is a very simplified example of a consumer (the class
ProducerExample) of the
IProducer strategy. The consumer explicitly checks if the strategy is null before invoking it, and if it is null, it produces some default value instead.
This extra null check gets in the way of what the class is actually doing and introduces an extra code path that is not wanted (or needed).
(The producer here is deliberately simple—in a real codebase this could take any number of arguments.)
One situation where this kind of code might appear is when a previously-untested class is being tested for the first time. In this case, dummy values might be inserted in order that
null can be passed as the strategy at test time and you'll still get results instead of a
NullReferenceException. This kind of setup is dangerous as inadvertently passing
null in the production code will end up generating these dummy values! 💥
Here's a very similar example, using a strategy that doesn't have a result (which I'll call a "handler"). This is checked for nullness before it is invoked for its side-effects:
Solution: Don't check strategies for nullness!
Instead of explicitly checking for nullness, assume that the strategy is not null (you can enforce this in the constructor) and migrate any "default action" into a new implementation of the strategy.
Then, anywhere you were previously passing in
null, you can instead pass an instance of your shiny new implementation.
For the producer example, we can move the default implementation into a new class, and remove the null check:
[NotNull] annotation here is something supported by Resharper via the Resharper.Annotations package.)
If we need to change the result of the producer depending on what the consumer is, we can use a variation on this, and create an implementation that stores any value we want:
In the "handler" case, we can create a 'null object' implementation that does nothing, since we don't need to produce a result:
With the refactored code, the consuming classes are cleaner (less code, lower cyclomatic complexity), and we have extracted a "default" implementation, which could potentially be used by other consumers.
At the same time we have created some useful additional implementations that we can use in unit tests! The "Default" or "Constant" producers are useful when providing canned data to classes that are being tested, and "Null" handlers are useful when ignoring part of the behaviour of a class in order to test other parts.
Variation: Function-oriented implementation
For single-method interfaces such as those above we can replace them with delegates. This can lead to much cleaner code.
And for handlers:
I have yet to explore this style in-depth myself, but it seems promising. (I would avoid using pure
Func<T, ...> as it doesn't give any indication of what the intention of the code is.)
The nice thing about delegates is that they will implicitly convert any compatible lambda; so if you need a one-off implementation in test code, you can write it directly in your test method, and not have to create an entirely new class.
Here I'm going to use strategy in a broad sense, to basically include any behaviour that is injected into a class. Other people might call these collaborators (although I'd apply that to the implementations of the strategy and not the strategy abstraction itself), or something different. ↩