Skip to content

Design Patterns: Strategy

Published: at 02:44 PM

Introduction

In software development, creating flexible and maintainable code is a constant challenge, especially when dealing with various requirements and behaviors. The Strategy design pattern offers a powerful solution by enabling developers to define a family of algorithms, encapsulate each algorithm into a separate class, and make their objects interchangeable. This design pattern not only promotes adherence to the Open/Closed Principle (OCP) but also enhances code readability and scalability.

In this article, we’ll explore the core concepts of the Strategy design pattern, understand its real-world applications, and walk through an example to see how it simplifies complex problems.

Table of contents

Open Table of contents

Definition

The Strategy design pattern is a behavioral design pattern that lets you create a family of algorithms, place each one into a separate class, and make their objects interchangeable.

The Strategy design pattern is used when an object performs a specific task in many different ways. In this case, these functionalities can be extracted into several separate classes called strategies.

The original object that works with the strategy is called the Context. The Context object is not responsible for selecting the appropriate strategy to delegate a task. Instead, the client should pass the desired strategy to the Context.

This way, the context stays independent of how the strategies are created, and a new strategy can be added without modifying the existing ones. The context is not aware of what kind of strategy is used because it works with the interface of the strategy.

What kind of problems can the Strategy design pattern solve?

  1. Diverse Algorithm Selection: When you need to choose between different algorithms to perform a task, but the choice of algorithm depends on runtime data or conditions. In this case, you can define a family of interchangeable algorithms, allowing the client to select an appropriate one at runtime. This ensures that an object’s behavior can change dynamically without modifying the algorithm itself.

  2. Encapsulating Behavioral Variations: An object may need to perform similar actions in different ways depending on the context or environment. The Strategy design pattern encapsulates these variations into separate strategy classes, enabling the object to stay focused on its core responsibilities while delegating specific behaviors to the appropriate strategy.

  3. Easily Adding New Behaviors: Anticipating the addition of new behaviors or algorithms in the future can be challenging without disrupting the existing system. By defining behavior through strategies, adding new functionality only requires creating new strategy classes. This avoids modifying the existing codebase, making the system easier to scale and extend.

What solutions does the Strategy design pattern describe?

  1. Implement the interface for the algorithm to make the algorithms interchangeable.

  2. Develop the algorithms based on this interface.

UML Diagram

UML Diagram

The client is not aware of which concrete algorithm it uses. Instead, it must have a field to store a reference to the desired strategy. The Strategy interface is common to all concrete strategies. Strategy1 and Strategy2 implement different variations of the same algorithm.

Example

Very often, we need to send data to analytics systems such as Amplitude, Firebase, AppsFlyer, or others.

To handle different analytics systems in a flexible way, we start by defining a protocol IAnalyticsStrategy. This protocol ensures that all analytics strategies implement a logEvent method, which takes an event name and an optional dictionary of parameters. The strategy pattern allows us to easily switch between different implementations without changing the code that uses the analytics system.

protocol IAnalyticsStrategy {
    func logEvent(name: String, parameters: [String: Any]?)
}

Let’s implement sending analytics data to two analytics systems, such as Amplitude and Firebase.

final class AmplitudeAnalytics: IAnalyticsStrategy {
    func logEvent(name: String, parameters: [String: Any]?) {
        print("Logging event to Amplitude: \(name), with parameters: \(parameters ?? [:])")
    }
}

final class FirebaseAnalytics: IAnalyticsStrategy {
    func logEvent(name: String, parameters: [String: Any]?) {
        print("Logging event to Firebase: \(name), with parameters: \(parameters ?? [:])")
    }
}

The AnalyticsManager class acts as the context for using the different analytics strategies. It holds a reference to a strategy (IAnalyticsStrategy) and delegates the task of logging events to the current strategy. The updateStrategy method allows dynamically switching the strategy at runtime, making it flexible and extensible.

final class AnalyticsManager {
    // MARK: Properties
    private var strategy: IAnalyticsStrategy
  
    // MARK: Initialization

    init(strategy: IAnalyticsStrategy) {
        self.strategy = strategy
    }
  
    func updateStrategy(_ newStrategy: IAnalyticsStrategy) {
        self.strategy = newStrategy
    }
  
    func trackEvent(name: String, parameters: [String: Any]? = nil) {
        strategy.logEvent(name: name, parameters: parameters)
    }
}

In this final part, we instantiate the AmplitudeAnalytics and FirebaseAnalytics strategies. Initially, we pass the AmplitudeAnalytics strategy to the AnalyticsManager, and track an event. After switching to the FirebaseAnalytics strategy using updateStrategy, we track a new event. This demonstrates how easily we can switch between different analytics services without modifying the core logic of tracking events.

let amplitudeAnalytics = AmplitudeAnalytics()
let firebaseAnalytics = FirebaseAnalytics()

let analyticsManager = AnalyticsManager(strategy: amplitudeAnalytics)

analyticsManager.trackEvent(name: "User Signed Up", parameters: ["method": "email"])

analyticsManager.updateStrategy(firebaseAnalytics)
analyticsManager.trackEvent(name: "User Logged In", parameters: ["platform": "iOS"])

Summary

The Strategy design pattern is a powerful tool for promoting flexibility and maintainability in software development. By decoupling algorithms from the context in which they are used, it enables dynamic behavior changes and reduces the complexity of code. This pattern is especially valuable when an application requires multiple approaches to solve a problem or when algorithms evolve over time. While it introduces some additional complexity due to the need for extra classes, the benefits of cleaner, more modular, and easily testable code far outweigh the drawbacks.

Thanks for reading

If you enjoyed this post, be sure to follow me on Twitter to keep up with the new content.


avatar

Nikita Vasilev

A software engineer with over 7 years of experience in the industry. Writes this blog and builds open source frameworks.


Next Post
Design Patterns: Decorator