In the previous post I worked through how to use generics with the enum constraints to allow for IMathOperator
to be constrained by an enum.
There is a lot of boiler plate code with the implementation of the strategy pattern and that can lead to code churn and pattern divergence.
If we can have interfaces which make implementations consistent then this can help with readable and maintainable code.
Working from my quote from the previous post let’s take a look at a way to define the required items to implement the strategy pattern and how to implement them in a consistent and maintainable way.
IStrategy Interface
We need to start with the top level interface definition; this is the pattern we are looking to implement afterall.
public interface IStrategy<in TTarget, in TCondition>
{
void Process(TTarget target, TCondition condition);
}
This on the surface looks pretty straight forward; we want to process the target type depending on the condition specified. Both the TTarget
and TCondition
are specified as generic constraints and neither are restricted. We don’t want to limit what the types can be at this point.
IStrategyProcessor Interface
The next part of the strategy pattern is the individual items which actually do the processing. These individual items, such as the AddOperator
from the previous example, need to determine if they are applicable under certain conditions and apply the required update.
public interface IStrategyProcessor<in TTarget, in TCondition>
{
bool CanUpdate(TCondition condition);
void Update(TTarget target);
}
Now I’ve decided to call it Update
however this could be called Process
or Apply
just depends on your terminology you want to use. The corresponding condition check method should ideally correspond with the terminology chosen.
To make the intent clear you can use a marker interface which derives from IStrategy
and IStrategyProcessor
which defines the types and then implement those. The whole point of this is to keep the code base consistent and maintainable.
StrategyBase
To keep the boiler plate code to a minimum it’s not a bad idea to have a base class which does a lot of the standard process flow of the pattern. In this instance we need something which will take in a collection of processors and iterate over them all when called. For this I have created a StrategyBase
abstract class.
public abstract class StrategyBase<TTarget, TCondition> : IStrategy<TTarget, TCondition>
{
private readonly IEnumerable<IStrategyProcessor<TTarget, TCondition>> _processors;
protected StrategyBase(IEnumerable<IStrategyProcessor<TTarget, TCondition>> processors)
{
_processors = processors;
}
public void Process(TTarget target, TCondition condition)
{
foreach (var processor in _processors)
{
if (processor.CanUpdate(condition))
{
processor.Update(target);
}
}
}
}
This has all the requirements to run the processors and allow for the target to be updated. It does however have restrictions as the generic constraints are the same for the Stategy and the Processors. I would guess this would not be an issue for a mjority of the implementations and solve the boiler plate code issue in the code base.
You would then create an implementation of the base class for each of the required processing requirements you have.
For example, here is a outline of how to write an implementation using the above.
/// <summary>
/// Sample base class for example
/// </summary>
public class Message
{
public string Data { get; set; }
}
public class Example : StrategyBase<Message, bool>
{
public Example(IEnumerable<IStrategyProcessor<Message, bool>> processors) : base(processors)
{
}
}
public class ExampleProcessor : IStrategyProcessor<Message, bool>
{
public bool CanUpdate(bool condition)
{
// removed for brevity
}
public void Update(Message target)
{
// removed for brevity
}
}
Extensions using the interfaces
I did mention that the StrategyBase
abstract base class had some restrictions due to the generic constraints. If for some reason you require some more complex processing then the IStrategy
and IStrategyProcessor
interfaces will still work but you will need to write the implementations yourself.
Let’s explore a more complex example.
Say I want to add in some additional complexity depending on how the strategy is called so the condition I want to pass into the strategy isn’t just a simple value it needs to check but a Func<>
to be evaluted before running each processor to then potentially check the value further?
I can define the Strategy API surface with implementing the interface.
public interface IComplexStrategy : IStrategy<Wrapper, Func<Wrapper, bool>>
{ }
And each of the processor implementaions still only want to determine the basic conditional return value so the constrained types are slightly different.
public interface IComplexStrategyProcessor : IStrategyProcessor<Message, bool>
{ }
This looks fine so far right?
So the processor itself will be similar to what we have seen before:
public class ComplexProcesserOne : IComplexStrategyProcessor
{
public bool CanUpdate(bool condition)
{
return condition; // or some other condition checking
}
public void Update(Message target)
{
// process the target
}
}
We need an implementation of the IComplexStratgy
to consume these processors:
public class ComplexStrategy : IComplexStrategy
{
private readonly IEnumerable<IComplexStrategyProcessor> _processors;
public ComplexStrategy(IEnumerable<IComplexStrategyProcessor> processors)
{
_processors = processors;
}
public void Process(Message target, Func<Message, bool> condition)
{
foreach (var strategyProcessor in _processors)
{
if (strategyProcessor.CanUpdate(condition(target)))
{
strategyProcessor.Update(target);
}
}
}
}
Notice that the strategyProcessor.CanUpdate method accepts a bool
value but this has run through the Func<>
specified by the caller on the target message first.
This would then be called by passing in a delgate or lambda function into each call. For instance you may only want the message to be updated if there isn’t a message already present in the Data
.
complexStrategy.Process(message, m => string.IsNullOrWhiteSpace(m.Data));
Another option would be changing the signature of the CanUpdate
method on the IComplexStrategyProcessor
so that it can return the Message
itself to perform further checking of the message per processor. The options are endless.
Conclusion
In this post we have looked at reducing the boiler plate code we have in a code base when implementing a number of various strategy pattern type scenarios. We have looked at the base class to avoid having to write the processor calling code multiple times. We have also looked at how the base interfaces can also be used for defining more complex options. Both allowing for the code base to extend but also maintain its consistency and readability.
Any questions/comments then please contact me on Twitter @WestDiscGolf