Hello everyone, friends. My name is Alex and I am a professional software developer and creator in the web industry. I have been studying languages for many years, sharing my experience with others.
Today I want to talk to you about the Strategy design pattern. I will try to convey to you the principles and essence of the template without water, and show you how to apply it in practice.
What’s the point?
Design patter Strategy refers to behavioral design patterns. It’s task is to identify similar algorithms that solve a specific problem. The implementation of the algorithms is taken out in separate classes and the ability to select algorithms is provided at runtime.
The template makes it possible in the process of execution to choose a strategy (algorithm, tool, approach) for solving the problem.
What’s the problem?
Let us consider the tasks in the solution of which this approach can be applied.
Imagine that you are faced with the task of writing a web portal for realty search. MVP (Minimum Viable Product) was designed and prioritized by your team of Product Managers and the portal should have functionality for apartment buyers. That’s, the target users of your product are, first of all, those who are looking for a new home to buy. One of the most requested features should be the ability to:
- Select the area on the map where the buyer wishes to purchase housing
- And indicate the price range of prices for apartments to filter that.
The first version of your portal did an excellent job and users could search without problems, narrowing their search for apartments by price range and the selected geographical area on the map. And of course, you did your best as a developer and did everything right in your opinion from the point of view of the code architecture, implemented classes that are looking for apartments for sale in your database.
But then Product Managers come to you and say that you need to add the ability to search and display realty for rent. We have one more type of user — tenants. It’s not so important for tenants to show price filters, they care about the condition of the apartment, so they need to display photos of the rented apartments.
Of course, it didn’t end there. In the near future, we plan to add the functionality of the work of legal entities, the functionality of payment and booking apartments immediately on the site. Further more — add the ability to view the history of realty, request a package of documents for a transaction and contact the owners, apply for a loan, and so on.
While the search and filtering functionality with apartments for sale was fairly easy to implement, any new changes caused a lot of questions and architecture headaches. I had to change a lot in the code. You understood that any change in the algorithms for issuing the necessary apartments and elements for display affects the main base classes, in which all the filtering functionality is implemented. The main functionality of searching for apartments was initially implemented in one class, with the addition of new functionality this class grew, you added new conditions, new branches, new methods and functions.
Changing such a class, even in the case of bug fixes, greatly increases the risk of making new bugs. A whole team of developers worked on this functionality, any changes eventually went through a long run-in and testing, and the changes added by other programmers to the base classes created new problems and conflicts, releases were delayed. You are facing the following problems:
- The main algorithm for finding apartments was implemented in one super-class.
- The algorithm for selecting and displaying interface elements was implemented in one super-class.
- Changes in these classes made by different programmers led to conflicts and the need for regression testing.
- Product releases were delayed, the time for developing new functionality increased several times.
- More time was spent on development, testing, a lot of bugs appeared.
What’s the solution?
In this example, we have several algorithms for one function:
- Searching for apartments for sale
- Searching for apartments for rent
- Displaying different sets of filters or not
- Displaying various interface elements — photos, booking buttons, feedback buttons, etc.
The Strategy design pattern solves this problem. He proposes to single out a family of similar algorithms, to bring them into separate classes. This will allow you to easily change the desired algorithm, extend it, minimizing development conflicts, dependencies on other classes and functionality. Instead of implementing the algorithm in a single class, our class will work with objects of strategy classes through the context object and delegate work to the desired object at the right time. To change the algorithm, it is enough to substitute the required strategy-object into the context at the right time.
For our class to work the same for different behaviors, strategy objects must have a common interface. By using such an interface, you make our context class independent of the strategy-classes.
Returning to our example with a real estate search portal, we must move the algorithms for searching for real estate for sale and rent into separate strategy classes, where we implement specific algorithms for finding different types of objects. The same can be done with classes for working with interface elements for different types of users.
In the strategy-classes that implement the search for real estate objects, only one method will be defined, doSearch (filters), which accepts filters as an argument, which will be used to search for specific objects using a concrete algorithm.
Despite the fact that each strategy class implements the search according to its own algorithm, using the necessary filters (sale, rent, country house, residential, not residential, etc.), it is possible, by the way, that they will work with different bases — all this for the web interface, which displays a list of found objects, does not matter. After the user has selected the type of real estate he is interested in in the filters on the site, a request will be made to the controller on the backend, with an action to receive data on incoming filters and user types.
The task of the controller is to define the strategy class and request the data to be displayed from the context class, passing it a known set of filters. The context class in this schema is a class that implements the method of searching for apartments by specified filters. In the class diagram above, we can see that the context class defines the getData method, and accepts filters as arguments. It must have a constructor that takes the currently active strategy-object and a setStrategy setter that sets the active strategy. This method is useful for the case when the user changes the type of the desired object, for example, he is looking for a property for sale and wants to rent an apartment.
Below we will consider an example of how the described task is solved in the GOlang language. You can get acquainted with a more detailed description of the Strategy pattern, as well as, practically, with all known patterns, on the Design Patterns course. The course discusses the problems in detail and shows examples of implementation in PHP. In addition to behavioral patterns, all types of patterns are considered, from fundamental to architectural. More than 70 lessons will allow you to master the field of application of the best software architecture practices for your projects.
Let’s go back to our example. The first thing we’ll do is define an interface with the doSearch method:
This method defines general behavior for specific algorithms that implement different strategies. The method can take various arguments, allowing you to implement branching in your algorithms. In the example, I am passing custom filters of type Map.
To implement specific algorithms, we create two files. Each file defines its own defined type with the base type struct, which implement the Strategy interface. Accordingly, custom filters will be passed to the methods defined by the interface for each algorithm. The implementation looks like this:
Let’s take a look at our class diagram. It remains for us to implement the context class and the client code for calling specific algorithms at the right time. How to do it? To create a layer of the context class, we implement the source that implements:
- a defined type in the base struct type,
- the initStrategy function, which initializes the default strategy and custom filters
- a method of type struct setStrategy, which sets the active strategy
- and the getData function, which calls a specific strategy and returns data to be shown to the user.
The last step is to look at the client code. First, the default strategy is initialized, we get data from this strategy. Then we expose a new active strategy and call the getData function for receiving data again. As you can see, the same function of the file-context (or class-context in the class diagram) is called each time to get the rendering data, which is subsequently displayed to the user. For simplicity, I am outputting debug information to the console to make sure different algorithms work when called. Here is the code:
Here’s the takeaway from this approach:
First implements strategy map[role:1]
Second implements strategy map[role:2]
As you can see, we can control the invocation of different algorithms depending on the context and custom filters. Algorithms can branch based on input filters and other parameters passed from client code to methods that implement specific algorithms. Here’s an interesting example. The object-oriented approach can be seen in the lecture of the PHP course, which discusses this pattern in detail and many others.
When to apply?
Finally, let’s talk about when the Strategy pattern is used?
- You have many similar implementations with some minor behavior. You can move the distinguishing behavior into strategy classes, and reduce the repeating code to a single context class.
- Your algorithm is implemented in a super class with multiple conditionals. Separate the blocks of conditional statements into separate strategy classes, and entrust the control of the calls to the context class.
- Concrete strategies allow algorithms to be encapsulated in their own concrete classes. Use this approach to reduce dependencies on other classes.
- Depending on the situation, you can change the strategy of the task during the program execution. For example, depending on the speed of the Internet, use different behavioral strategies that return a different set of data for displaying the page.
Friends, we got acquainted with the behavioral design pattern Strategy. The template is used to highlight similar algorithms that solve a specific problem. We looked at the implementation in the GOlang language with you, got acquainted with the possibilities of the approach and figured out when it is better to use it.
If you liked the article and want to know more about the architecture of software products, subscribe to my feeds on the networks. From time to time I post interesting materials about the architecture of software products. Good luck to everyone, Alex Versus (Udemy, Facebook, Youtube).