Angular Design Patterns: Strategy Pattern

Credits to https://www.pexels.com/photo/smoothed-by-wind-dunes-16908885/
Credits to https://www.pexels.com/photo/smoothed-by-wind-dunes-16908885/

Intro

The Strategy pattern is a behavioral design pattern that provides a mechanism to select an algorithm at runtime from a family of algorithms, and make them interchangeable. In context of Angular, you can think algorithms as services here but it can also be used for components and classes.

🗎 Source Code

➡ī¸ Recommended Prerequisite

💎Medium Friendly Link

Key benefits

  • Purpose: To define a family of interchangeable algorithms and allow the client to choose dynamically which one to use at runtime. This provides flexibility.

  • Encapsulation: Each strategy is encapsulated as a separate class, which helps keep the code clean and organized. The client code does not know the details of how each strategy is implemented, as it only interacts with the common interface.

  • Composition over inheritance: Strategy pattern is based on composition to achieve behavior reuse instead of relying on inheritance. This leads to more flexible designs.

Useful Scenarios 💎

  • When you have multiple algorithms or approaches that can be used interchangeably to solve a problem.

Classic Scenarios

Navigation app: A navigation might use different routing strategies for cars, pedestrians, or cyclists.

Sorting algorithms: Different sorting algorithms (quicksort, bubble sort, merge sort) can be implemented as strategies, letting you choose the most suitable one at runtime.

Tip

An important advantage of the Strategy patttern is to provide users to choose different strategies at runtime.✨

Glossary 🌍

Note

Glossary is going to make more sense when we visit our example.

1. Context:

The Context has a reference to one of the concrete strategies and which is used by its other methods. To achieve it, the context class usually has a public method called setStategy(stategy) which is set by client. That reference is type of Strategy Interface. The context does not know which strategy it uses, the key point is that strategy should implement Strategy Interface.

2. Strategy interface:

It is common to all concrete strategies. It defines a blueprint which is implemented by concrete strategies.

3. Concrete Strategies:

Concrete Strategies are implementations of algorithms that is used by client and used by context. They implement Strategy Interface.

4. The Client: initializes a specific strategy object and passes it to the context via setStategy(stategy)

Examples

Example 1: Shipping app

Assume you have e-commerce application. You want to provide shipping information based on client's preference.

In the context of this post, you want to provide interchangeable strategies to choose from.

Presentation

Problem

Let's first examine naive/brute force approach: âŦ‡ī¸

Source Code V1

export class ShippingV1Component {
  public readonly shippingOptions = ['EXPRESS', 'ECONOMY'];
  public selectedOption!: string;
  public type?: string;
  public cost?: string;
  public estimatedTime?: string;

  constructor(
    private readonly expressShippping: ExpressShippingService,
    private readonly economyShipping: EconomyShippingService
  ) {}

  public onStrategyChange(option: string): void {
    this.selectedOption = option;
    this.getData(option);
  }

  private getData(option: string): void {
    if (option === 'EXPRESS') {
      this.type = this.expressShippping.getType();
      this.cost = this.expressShippping.getCost();
      this.estimatedTime = this.expressShippping.getEstimatedTime();
    } else if (option === 'ECONOMY') {
      this.type = this.economyShipping.getType();
      this.cost = this.economyShipping.getCost();
      this.estimatedTime = this.economyShipping.getEstimatedTime();
    }
  }
}

As you see on getData method we already have undesired condition. This can get more complicated. What if we introduce new shipping services such as Sea Shipping and so on? We are going to have more complexity. 😕

Solution: 🛠

The diagram below shows how our final implementation should look like:

Step 1: Define a strategy interface for concrete strategies

The Shipping Strategy interface is common to all variants of the strategies.

export interface IShippingStrategy {
  getType: () => string;
  getCost: () => string;
  getEstimatedTime: () => string;
}

Step 2: Create concrete strategies which implements IShippingStrategy

@Injectable({
  providedIn: 'root',
})
export class EconomyShippingService implements IShippingStrategy {
  public getType(): string {
    return 'ECONOMY';
  }

  public getCost(): string {
    return '15$';
  }

  public getEstimatedTime(): string {
    return '5-12 days';
  }
}

and

@Injectable({
  providedIn: 'root',
})
export class ExpressShippingService implements IShippingStrategy {
  public getType(): string {
    return 'EXPRESS';
  }

  public getCost(): string {
    return '100$';
  }

  public getEstimatedTime(): string {
    return '1-2 days';
  }
}

Step 3: Create a context service which has reference to concrete strategies.

@Injectable({
  providedIn: 'root',
})
export class ShippingContextService implements IShippingStrategy {
  private strategy!: IShippingStrategy;

  public hasChosenStrategy(): boolean {
    return !!this.strategy;
  }

  public setStrategy(strategy: IShippingStrategy): void {
    this.strategy = strategy;
  }

  public getType(): string {
    return this.strategy.getType();
  }

  public getCost(): string {
    return this.strategy.getCost();
  }

  public getEstimatedTime(): string {
    return this.strategy.getEstimatedTime();
  }
}

The setStrategy method is called by client.

Note

The context is not limited to IShippingStrategy interface. You can also have specific methods that is shared by all concrete stategies.

Step 4: Allow client to choose preferred strategy.

Our new ShippingV2Component:

Source Code V2

export class ShippingV2Component {
  public readonly shippingOptions = ['EXPRESS', 'ECONOMY'];
  public selectedOption!: string;
  public type?: string;
  public cost?: string;
  public estimatedTime?: string;

  constructor(
    private readonly injector: Injector,
    private readonly shippingContext: ShippingContextService
  ) {}

  public onStrategyChange(option: string): void {
    this.selectedOption = option;

    switch (option) {
      case 'EXPRESS': {
        const strategy = this.injector.get(ExpressShippingService);
        this.shippingContext.setStrategy(strategy);
        break;
      }

      case 'ECONOMY': {
        const strategy = this.injector.get(EconomyShippingService);
        this.shippingContext.setStrategy(strategy);
        break;
      }
    }
    this.getData();
  }

  private getData(): void {
    if (!this.shippingContext.hasChosenStrategy) {
      return;
    }
    this.type = this.shippingContext.getType();
    this.cost = this.shippingContext.getCost();
    this.estimatedTime = this.shippingContext.getEstimatedTime();
  }
}

As you noticed we use condition only once at onStrategyChange when the Client chooses shipping option.

That's it 🙂

Example 2: Revising Notes app via Factory Pattern

Please read the previous post of Factory Pattern for this example.

In the post of Factory Pattern I put this warning:

Warning

Do you notice the issue here? On every call we call createNoteService which creates and returns new reference necessarily. This can cause memory leak. To improve it we can create service on demand when there is a change in network. This solution below resembles strategy pattern.

Then we provided the updated solution for this problem via Strategy Pattern.

That's all hope you enjoyed it and found it useful. Thanks for reading.🍍

References:

https://refactoring.guru/design-patterns/strategy

AI tools