Implementation

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?

Class diagram

The class diagram below shows the implementation of the Strategy design pattern:

Class Diagram — 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.

IShippingCostsStrategy

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.

Order

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).

OrderItem

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.

PackageSize

A special kind of class — enumeration — to define different package size of the order item.

Example

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.