The main reasons design patterns can be simplified are:
1. A Design Pattern is baked into the language. This is the case with Iterator (implemented with IEnumerator and foreach) and Observer (Events).
2. The ability to reference methods in forms of delegates gives more flexibility than having specific classes that hold implementation called through some interface.
We will not talk about the first category because we use these features instinctively these days. However, the second category brings an interesting twist on existing design patterns. We will explore solutions through the angle of game development, and if you're coming from the more web development perspective, things to keep in mind are: a) Performance is critical. b) Instantiation is a necessary evil, as a specific of (a).
Strategy:
Allows the change of behavior of a method at runtime.
The problem this pattern solves is having to change the way something is done at runtime. This most commonly refers to some algorithm being swapped out dynamically, chosen at runtime from a family of algorithms, all having the same interface.
The key to the pattern is to have an object that calls a certain behavior through an interface. The specific implementation of that interface will be a class that only holds that algorithm.
Drawbacks:
- Introduces classes that represent behavior, which are not an ideal conceptual fit. Behavior related to a class is inside another class. If the behavior wasn't switchable, it would have been inside the class as a normal method. So, in a classic implementation, creating new classes for behaviors is a "technical necessity" rather than a feature of good expected design.
- Possible number of classes explosion.
- Calls to the behavior must come through an interface, whether that is desirable or not - as per consideration (a) above.
- Possible reckless creation of new objects when switching a strategy - as per consideration (b) above.
Classic implementation example:
class Program {
static void Main(string[] args) {
var myScrewdriver = new ParticularScrewdriverModel(new PhillipsHead());
TestScrewdriver(myScrewdriver);
myScrewdriver.ChangeHead(new SlottedHead());
TestScrewdriver(myScrewdriver);
myScrewdriver.ChangeHead(new HexHead());
TestScrewdriver(myScrewdriver);
Console.ReadKey();
}
static void TestScrewdriver(IModularScrewdriver screwdriver) {
Console.WriteLine(screwdriver.Turn());
}
}
public interface IScrewdriverHead {
string Turn();
}
public struct PhillipsHead : IScrewdriverHead {
public string Turn() => "Turning PhillipsHead";
}
public struct SlottedHead : IScrewdriverHead {
public string Turn() => "Turning Slotted";
}
public struct HexHead : IScrewdriverHead {
public string Turn() => "Turning Hex";
}
public interface IModularScrewdriver {
string Turn();
void ChangeHead(IScrewdriverHead iScrewdriver);
}
public class ParticularScrewdriverModel : IModularScrewdriver {
private IScrewdriverHead _head;
public string Turn() => _head.Turn();
public ParticularScrewdriverModel(IScrewdriverHead head) {
ChangeHead(head);
}
public void ChangeHead(IScrewdriverHead head) {
_head = head;
}
}
Explanation: this is the simplest example of Strategy I could come up with. Simply, we are changing a head on a "modular screwdriver" to represent the switch of algorithms. The problem from the last point is apparent here: what happens with the old instances of strategy objects whenever we change a strategy? They will simply be left up to the GC to collect when the time comes. This means that in this version we are generating a small amount of garbage every time we switch a strategy. This can be solved by using Structs on strategies themselves and while that will work, most people don't do it that way. In that case, the strategy instance will simply be embedded inside the class instance (the strategy owner, here ParticularScrewdriverModel). Another solution could be to make the strategies themselves state-less Singletons and rout all data through the method. I don't really like this approach because it decreases locality and creates more complexity in this already too complex code for what it's doing.
More modern example:
class Program {
static void Main(string[] args) {
var myScrewdriver = new ParticularScrewdriverModel(ScrewdriverHeadType.PhillipsHead);
TestScrewdriver(myScrewdriver);
myScrewdriver.ChangeHead(ScrewdriverHeadType.SlottedHead);
TestScrewdriver(myScrewdriver);
myScrewdriver.ChangeHead(ScrewdriverHeadType.HexHead);
TestScrewdriver(myScrewdriver);
Console.ReadKey();
}
static void TestScrewdriver(IModularScrewdriver screwdriver) {
Console.WriteLine(screwdriver.Turn());
}
}
public interface IModularScrewdriver {
string Turn();
void ChangeHead(ScrewdriverHeadType headType);
}
public enum ScrewdriverHeadType {
PhillipsHead, SlottedHead, HexHead
}
public class ParticularScrewdriverModel : IModularScrewdriver {
private Func<string> _head;
public string Turn() => _head?.Invoke();
public ParticularScrewdriverModel(ScrewdriverHeadType headType) {
ChangeHead(headType);
}
public void ChangeHead(ScrewdriverHeadType headType) {
switch(headType) {
case ScrewdriverHeadType.PhillipsHead:
_head = PhillipsHead;
break;
case ScrewdriverHeadType.SlottedHead:
_head = SlottedHead;
break;
case ScrewdriverHeadType.HexHead:
_head = HexHead;
break;
default: throw new ArgumentException("Unexpected Head type: " + headType);
}
}
string PhillipsHead() => "Turning PhillipsHead";
string SlottedHead() => "Turning Slotted";
string HexHead() => "Turning Hex";
}
There are a couple of design choices made here. First, we could have easily injected the strategy itself in the c-tor of the class and the ChangeHead() method. There are a coupe of problems I have with this approach: It holds the implementation away from the class that needs it. Thus, whoever actually has the implementing method knows too much about the class that's actually doing the operation. The particular details of the strategy should be inside the class that's doing the strategy or inside the separate class like before, not inside the class that does the actual switching (in our case static Main()). This variation would then look more like Template Method Pattern.
The argument can certainly be made that now implementation is defined by 2 things: the enum ScrewdriverHeadType
and the method itself, so that every time we want to add a new strategy we need to add 2 things instead of just one (1 enum + 1 method VS just 1 class). The thinking is that this hampers discoverability for the programmer and introduces the possibility of error. I do not agree with this. In practice, the programmer must always first realize the interface through which something is called before realizing this interface needs to be extended (of course), so it will be impossible to miss the existence of the enum. The programmer extending also needs to remember to change the ChangeHead method, and since that involves change in an existing method, it can be argued that it violates Open-Closed Principle in the purest sense. Yet the same idiom is used in Factory style patterns and we didn't complain. With something like discriminated unions language feature things would have been a little bit more pure, but we don't have those in C# yet.
Another argument can be made that this version hampers extensibility: the caller cannot create their own strategy. The extensibility in this case has nothing to do with a design pattern, and may or may not be a desirable separate feature. It can be an added benefit, but it isn't necessary for this pattern.
Also, this whole design pattern simplification could have been done even without delegates. If we simply held a private field with the enum and did the switch when calling the Turn() method. Not to mention this allows us to easily check which strategy (or "mode") we're in right now, which isn't quite as trivial in a classic implementation.
If you're finding this article helpful, consider our asset Dialogical on the Unity Asset store for your game dialogues.
Template Method:
This pattern has certainly received much criticism because of it's ability to create class explosion. Let's say we have a situation where we want to sum numbers in an array only when they obey a certain rule. For example, right now we want to sum numbers that are odd or divisible by 5, but we may want to extend the behavior in the future. Also, combinations could be allowed in the future. We could use a Template method like so:
public abstract class SelectiveSummer {
public int Process(List<int> inputs) {
int result = 0;
foreach(var input in inputs) {
if(Filter(input)) result += input;
}
return result;
}
protected abstract bool Filter(int element);
}
public class EvenSummer : SelectiveSummer {
protected override bool Filter(int element) {
return (element % 2 == 0);
}
}
public class DivisibleBy5Summer : SelectiveSummer {
protected override bool Filter(int element) {
return (element % 5 == 0);
}
}
class Program {
static void Main(string[] args) {
List<int> inputs = new List<int>() { 1,2,3,4,5 };
Console.WriteLine((new EvenSummer()).Process(inputs));
Console.WriteLine((new DivisibleBy5Summer()).Process(inputs));
Console.ReadKey();
}
}
It's clear this approach doesn't work for this problem. Every time we want to create a new type of Summing operation we need to write a new class. Not to mention that for every use of yet unused summing operation we create a new class instance. Instead, we want to pass a method that describes the filtering - in effect we are fleshing out the template with the methods known at caller site. This method could either be anonymous or passed as a delegate.
public class SelectiveSummer {
public int Process(List<int> inputs, Func<int, bool> Filter) {
int result = 0;
foreach(var input in inputs) {
if(Filter != null && Filter.Invoke(input)) result += input;
}
return result;
}
}
class Program {
static void Main(string[] args) {
List<int> inputs = new List<int>() { 1,2,3,4,5 };
Console.WriteLine((new SelectiveSummer()).Process(inputs, x => x % 2 == 0));
Console.WriteLine((new SelectiveSummer()).Process(inputs, x => x % 5 == 0));
Console.ReadKey();
}
}
I would not say the Template Method Pattern is not useful, specifically when we want to limit the options on the call site. Sometimes the inheriting class would do many methods differently, and not all combinations of methods are valid. Overall, there is a particular type of problem where this variation is more desirable.
As a side note, this example could have been a one-liner in LINQ, but that is not the point. I could have used the Transaction Wrapper type of method to demonstrate the principle and LINQ would not help there. I also could have uses a static class SelectiveSummer
but I think that would detract from the example.
If you're finding this article helpful, consider our asset Dialogical on the Unity Asset store for your game dialogues.