Strategy Pattern
Introduces the Strategy Design Pattern and shows its potential in keeping your code base S.O.L.I.D. and clean. An example implementation of the pattern is provided in TypeScript.
Last updated
Introduces the Strategy Design Pattern and shows its potential in keeping your code base S.O.L.I.D. and clean. An example implementation of the pattern is provided in TypeScript.
Last updated
The Strategy Pattern is one of those design patterns that are remarkably simple in style yet efficient in function. Quite a few developers, when first getting in contact with a formal description of the pattern, realize they have been using it all along.
In this article I will introduce the strategy pattern and show its great potential for keeping your code base S.O.L.I.D. and clean. The code examples presented are implemented in TypeScript.
You can get the source code for this project on GitHub.
Imagine you are tasked with implementing a course management system for a college or university. More specifically, your current job is to list the registered students of a certain course (student enrollment) and print it out in some form. Let's quickly glance at the key players involved here:
The classes could look as follows:
So far, there's nothing remarkable going on there: Courses have an identifying nr
, name
and a list of participants
. Students have attributes such as an identifying nr
, firstName
, lastName
and a satScore
(see SAT score).
Now, printing the list of participants to an output device (we will stick with the Console
here to keep things simple) is a task that should be delegated to some sort of View
component. Let's create a CourseView
component and have it print out sample data through a printParticipants()
method:
We will quickly run our sample app and check the results.
This will give us the following output:
Great, the basics are in place and working. Printing the list of participants is still very rudimentary as it just returns the order of participants given in the sample data at design time. Usually any person viewing these lists has a certain informational need. In short, they would very much want the list to be sorted according to some criteria before it gets printed.
Let's modify the CourseView
class and have it sort the participants by their lastName
(in ascending order) before printing out the results:
We are using the Array.sort([compareFunction])
function for sorting, which is part of the ECMAScript Language Specficiation 5.1. It accepts as an argument a universal compareFunction
that defines the general sort order and that itself accepts two elements A
and B
and returns a negative number if A < B
(i.e. A
sorted before B
), 0 if A == B
(i.e. A
and B
equal) and a positive number if A > B
(i.e. A
sorted after B
). In our case, such comparing is delegated to the compareByLastNameAscending(a: Student, b: Student): number
method, which does exactly as it says: It compares the Student
s by their lastName
in ascending order.
This gives us:
Next, a new requirement request is thrown at you: The user now needs to sort the list dynamically according to two criteria:
lastName
, and
nr
(student number)
Dynamically here means that the user can pick at runtime which criterion to sort by. Ok, you think, no problem. You have sorted by one criterion, so now you simply sort by another. To allow the user to switch between the two, you will just have to add a switching mechanism that tells your view which sorting specifics to apply.
Let's get to it: Add a property sortByLastName: boolean
to the CourseView
class, which will act as a switch for the sorting criterion to apply. We will also add a new compare method private compareByScore(a: Student, b: Student): number
to compare students by their score
s. In our printParticipants()
method we will then have to pick the proper compare method ( as selected by our switch) and pass it to the Array.sort()
method:
Let's see how our app reacts when setting the sortByLastName
switch to false
and thus have our participants sorted by satScore:
Fine, that works! But this begins to feel awkward. We have only added another minor feature and our CourseView
class already beings to look bloated. If we keep adding more features, our class will soon burst at the seams. For now, it looks as though we might get away with this convoluted code arrangement.
But no! There we have it: a new feature request comes your way. This "getting away with" idea never really works out, does it?
The user now wants to sort the list of participants also by student nr
. You're sensing a pattern here. We cannot just keep adding new switches and compare methods to accompany every new sorting feature. The sortByLastName
switch is already a bad choice as it doesn't tell us anything about what happens if it is false
. If we were to add another switch, our code base would disintegrate completely.
Moreover, all these compare methods bloat up our class. Certainly, our CourseView
class is doing too many things at once and thus violating the Single Responsibility Principle (SRP). Now that you come to think about it, the class also doesn't adhere to the Open-Closed Principle (OCP): We cannot just add another sorting criterion without modifying the existing class structure. Our current code is definitely not closed to modification as the OCP demands.
If only there was a way to adhere to both SRP and OCP and be able to add custom sorting "strategies" without polluting our code! You think for a moment. Did you say "strategy"? You have an idea...
Can't we just delegate the entire sorting logic to a separate class, some kind of "strategy" that the user could pick and the view would adhere to? We could even go a step further and make it a class hierarchy so as to host an entire armada of different strategies for sorting. OurCourseView
class could then receive a property that references the current sorting strategy.
This setup would serve two purposes: First, the user could switch between different sorting methods as per their preferred sort criterion. Second, by decoupling the sorting logic from the view logic, we could implement new sorting methods in the future without having to touch our view class. SRP and OCP would both be satisfied, and our code would look much cleaner!
This very idea is exactly what the Strategy Design Pattern is all about: Separate the execution of some business logic from the entity that makes use of it and give this entity a way of switching between different forms of the logic. Thus, the currently selected concrete implementation of said business logic within the entity becomes a strategy that can be changed and swapped as need arises.
As a first step, we would have to create a common base class called SortingStrategy
. It could be made an abstract class and would only handle the sorting logic according to an internal compare method, the implementation of which it would delegate to its sub types. Our players and their relationships could then look something like this:
Let's see how we would implement the abstract SortingStrategy
base class:
One thing to note here is that within the sort logic we no longer operate on the original student array. Instead, we create a copy (Array.slice(0)
) and operate on this one, so as not to have any permanent side-effects (operating on the original array would have it changed every time we call the sort method).
And now to the implementation of the different sorting strategies. We would need at least three types:
LastNameAscendingSortingStrategy
: sorting by Student.lastName
(ascending order)
StudentNrAscendingSortingStrategy
: sorting by Student.nr
(ascending order)
SatScoreDescendingSortingStrategy
: sorting by Student.satScore
(descending order)
Our classes would be implemented as follows:
And our CourseView
class would receive a sortingStrategy
property, which would act as the new switch to give the client the ability to parameterize the desired sorting strategy. Also, the CourseView
would have to sort the list of participants according the the strategy currently assigned.
That's it. Very nice! We have cleaned up our code base and separated the sorting logic into its own class hierarchy. This is a code base now that can easily be maintained and extended without touching existing class hierarchies.
Now let's see our final work in action:
And our console would read:
That looks good. Our view and sorting strategies are working as expected.
One more thing to make our code even more readable: Look again at the compareStudents(a: Student, b: Student): number
method in our base class SortingStrategy
. The number returned really isn't up to any good. All it does is tell us how to sort two Student
instances: a < b
(returns -1
or any negative number), a == b
(returns 0
) or a > b
(returns 1
or any positive number). Let's try to make this a bit less subtle and more explicit. After all, TypeScript allows us to define number-based enums
. We could define our own enum
type and have it return those numbers behind more expressive names:
Our compareStudents(...)
method would then have to return the new ComparisonResult
enum type instead of a number
directly (here only exemplified using the SatScoreDescendingSortingStrategy
class):
Now, this change wraps it up perfectly! Your code base is now clean, readable and easily extendable. Great work! The Strategy Pattern has helped you a great deal in dealing with different ways of sorting the participants list.
You can get the source code for this project on GitHub.