Flutter Design Patterns: 5 — Strategy
An overview of the Strategy design pattern and its implementation in Dart and Flutter
In the last article, I have represented the Composite design pattern. This time, I would like to analyse and implement a design pattern which belongs to the category of behavioural design patterns — Strategy.
Table of Contents
- What is the Strategy design pattern?
- Other articles in this series
- Your contribution
What is the Strategy design pattern?
Strategy, also known as policy, belongs to the category of behavioural design patterns. The intention of this design pattern is described in the GoF book:
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
The Strategy is considered as one of the most practical design patterns, you can find a lot of uses for it in day-to-day coding. The main idea of this pattern is to extract related algorithms (or any piece of code) into separate classes and define a common interface for them. This enables compile-time flexibility — new algorithms can be added by defining new classes, existing ones can be changed independently. Also, the extracted strategy class can be changed in the code dynamically at run-time. Another advantage of the pattern is that allows you isolate the code, internal data, and dependencies of various algorithms from the rest of the code — clients use a simple interface to execute the algorithms and switch them at run-time. Possible use-cases of the Strategy design pattern:
- Sorting algorithms — each algorithm (e.g. bubble sort, quicksort, etc.) is extracted into a separate class, a common interface is defined which provides a method sort();
- Payment strategies — you want to define different payment options in your code (mobile payment, bank transfer, cash, credit card, you name it) and use them based on the user’s selection;
- Damage calculation in RPG game — there are several different types of attack in the game e.g. attacking with different moves, combos, spells, using weapons, etc. Several different algorithms could be defined for each attack type and the damage value could be calculated based on the context.
Let’s jump right in by analysing the Strategy design pattern and its implementation in more detail!
In the picture below you can see a general structure of the Strategy design pattern:
- Strategy — declares an interface which is common to all supported algorithms. It also declares a method the Context uses to execute a specific strategy;
- ConcreteStrategies — implement different algorithms using the Strategy interface which is used by the Context;
- Context — maintains a reference to a Strategy object, but is independent of how the algorithm is implemented;
- Client — creates a specific strategy object and passes it to the Context.
The primary purpose of the Strategy design pattern is to encapsulate a family of algorithms (related algorithms) such that they could be callable through a common interface, hence being interchangeable based on the specific situation. Also, this pattern should be considered when you want to use different calculation logic within an object and/or be able to switch between different algorithms at run-time. A general rule of thumb — if you notice different behaviours lumped into a single class, or there are several conditional statements in the code for selecting a specific algorithm based on some kind of context or business rules (multiple if/else blocks, switch statements), this is a big indicator that you should use the Strategy design pattern and encapsulate the calculation logic in separate classes (strategies). This idea promotes the Open-Closed Principle (the letter O in SOLID principles) since extending your code with new behaviour (algorithm) does not insist you to change the logic inside a single class, but allows creating a new strategy class instead — software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
The following example and the problem we want to resolve could look similar for some of you, which are using Flutter to build an e-commerce mobile application. Let’s say that your e-shop business offers several different shipping types for your customers:
- Picking up the ordered items from a physical store (or any other physical place, e.g. a warehouse);
- Sending order items using parcel terminal services;
- Sending order items directly to your customers in the shortest delivery time possible — priority shipping.
These three types contain different shipping costs calculation logic which should be determined at run-time, e.g. when the customer selects a specific shipping option in UI. At first sight, the most obvious solution (since we do not know which shipping option would be selected) is to define these algorithms in a single class and execute a specific calculation logic based on the customer’s choice. However, this implementation is not flexible, for instance, if you want to add a new shipping type in the future, you should adjust the class by implementing the new algorithm, at the same time adding more conditional statements in the code — this violates the Open-Closed principle, since you need to change the existing code for the upcoming business requirements. A better approach to this problem is to extract each algorithm into a separate class and define a common interface which will be used to inject the specific shipping costs calculation strategy into your code at run-time. Well, the Strategy design pattern is an obvious choice for this problem, isn’t it?
The class diagram below shows the implementation of the Strategy design pattern:
IShippingCostsStrategy defines a common interface for all the specific strategies:
- label — a text label of the strategy which is used in UI;
- calculate() — method to calculate shipping costs for the order. It uses information from the Order class object passed as a parameter.
InStorePickupStrategy, ParcelTerminalShippingStrategy and PriorityShippingStrategy are concrete implementations of the IShippingCostsStrategy interface. Each of the strategies provides a specific algorithm for the shipping costs calculation and defines it in the calculate() method.
StrategyExample widget stores all different shipping costs calculation strategies in the shippingCostsStrategyList variable.
An interface which defines methods and properties to be implemented by all supported algorithms. Dart language does not support the interface as a class type, so we define an interface by creating an abstract class and providing a method header (name, return type, parameters) without the default implementation.
Specific implementations of the IShippingCostsStrategy interface
InStorePickupStrategy implements the shipping strategy which requires the customer to pick-up the order in the store. Hence, there are no shipping costs and the calculate() method returns 0.
ParcelTerminalShippingStrategy implements the shipping strategy when order is delivered using the parcel terminal service. When using parcel terminals, each order item is sent separately and the shipping cost depends on the parcel size. The final shipping price is calculated by adding up the separate shipping cost of each order item.
PriorityShippingStrategy implements the shipping strategy which has a fixed shipping cost for a single order. In this case, the calculate() method returns a specific price of 9.99.
A simple class to store an order’s information. Order class contains a list of order items, provides a method to add a new OrderItem to the order, also defines a getter method price which returns the total price of the order (without shipping).
A simple class to store information of a single order item. OrderItem class contains properties to store order item’s title, price and the package (parcel) size. Also, the class exposes a named constructor OrderItem.random() which allows creating/generating an OrderItem with random property values.
A special kind of class — enumeration — to define different package size of the order item.
First of all, a markdown file is prepared and provided as a pattern’s description:
StrategyExample implements the example widget of the Strategy design pattern. It contains a list of different shipping strategies (shippingCostsStrategyList) and provides it to the ShippingOptions widget where the index of a specific strategy is selected by triggering the setSelectedStrategyIndex() method. Then, the selected strategy is injected into the OrderSummary widget where the final price of the order is calculated.
ShippingOptions widget handles the selection of a specific shipping strategy. The widget provides a radio button list item for each strategy in the shippingOptions list. After selecting a specific shipping strategy, the onChanged() method is triggered and the selected index is passed to the parent widget (StrategyExample). This implementation allows us to change the specific shipping costs calculation strategy at run-time.
OrderSummary widget uses the injected shipping strategy of type IShippingCostsStrategy for the final order’s price calculation. The widget only cares about the type of a shipping strategy, but not its specific implementation. Hence, we can provide different shipping costs calculation strategies of type IShippingCostsStrategy without making any changes to the UI.
The final result of the Strategy design pattern’s implementation looks like this:
As you can see in the example, the shipping costs calculation strategy could be changed at run-time and the total order price is recalculated.
All of the code changes for the Strategy design pattern and its example implementation could be found here.
👏 Press the clap button below to show your support and motivate me to write better!
💬 Leave a response to this article by providing your insights, comments or wishes for the series.
📢 Share this article with your friends, colleagues in social media.
➕ Follow me on Medium.
⭐ Star the Github repository.