Design Patterns: Strategy

 
  • Behavioural Pattern
  • Enables selecting an algorithm at runtime. Instead of implementing a single algorithm directly, code receives run-time instructions as to which in a family of algorithms to use.
  • Deferring the decision about which algorithm to use until runtime allows the calling code to be more flexible and reusable.
    • For instance,
    • a class that performs validation on incoming data may use the strategy pattern to select a validation algorithm depending on the type of data, the source of the data, user choice, or other discriminating factors.
    • These factors are not known until run-time and may require radically different validation to be performed.
    • The validation algorithms (strategies), encapsulated separately from the validating object, may be used by other validating objects in different areas of the system (or even different systems) without code duplication.
  • The strategy pattern stores a reference to some code in a data structure and retrieves it with function pointer, the first-class function, classes or class instances.
    • first-class function:
      • A programming language is said to have first-class functions if the language supports:
        • passing functions as arguments to other functions
        • returning them as the values from other functions
        • assigning them to variables
        • storing them in data structures

Strategy and SOLID:Open/Closed Principle

  • According to the strategy pattern, the behaviors of a class should not be inherited. Instead, they should be encapsulated using interfaces.
  • This is compatible with the Open/Closed Principle (OCP), which proposes that classes should be open for extension but closed for modification.
  • The strategy pattern uses composition instead of inheritance.
  • In the strategy pattern, behaviors are defined as separate interfaces and specific classes implement these interfaces.
  • This allows better decoupling between the behavior and the class that uses the behavior. The behavior can be changed without breaking the classes that use it, and the classes can switch between behaviors by changing the specific implementation used without requiring any significant code changes. Behaviors can also be changed at run-time as well as at design-time.

UML Class & Sequence diagrams

Strategy Pattern UML Class & Sequence diagrams

  • In the above UML class diagram,
    • The Context class doesn't implement an algorithm directly.
    • Instead, Context refers to the Strategy interface for performing an algorithm (strategy.algorithm()), which makes Context independent of how an algorithm is implemented.
    • The Strategy1 and Strategy2 classes implement the Strategy interface, that is, implement (encapsulate) an algorithm.
  • The UML sequence diagram shows the run-time interactions:
    • The Context object delegates an algorithm to different Strategy objects.
    • First, Context calls algorithm() on a Strategy1 object, which performs the algorithm and returns the result to Context.
    • Thereafter, Context changes its strategy and calls algorithm() on a Strategy2 object, which performs the algorithm and returns the result to Context.

Python Reference Implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import abc
from typing import List

class Bill:
    def __init__(self, billing_strategy:"BillingStrategy"):
        self.drinks: List[float] = []
        self._billing_strategy = billing_strategy

    @property
    def billing_strategy(self) -> "BillingStrategy":
        return self._billing_strategy

    @billing_strategy.setter
    def billing_strategy(self, billing_strategy: "BillingStrategy") -> None:
        self._billing_strategy = billing_strategy

    def add(self, price: float, quantity: int) -> None:
        self.drinks.append(self.billing_strategy.get_act_price(price * quantity))

    def __str__(self) -> str:
        return str(f{sum(self.drinks)}")

class BillingStrategy(abc.ABC):
    @abc.abstractmethod
    def get_act_price(self, raw_price: float) -> float:
        raise NotImplementedError


class NormalStrategy(BillingStrategy):
    def get_act_price(self, raw_price: float) -> float:
        return raw_price


class HappyHourStrategy(BillingStrategy):
    def get_act_price(self, raw_price: float) -> float:
        return raw_price * 0.5


def main() -> None:
    normal_strategy = NormalStrategy()
    happy_hour_strategy = HappyHourStrategy()

    customer_1 = Bill(normal_strategy)
    customer_2 = Bill(normal_strategy)

    # Normal billing
    customer_1.add(2.50, 3)
    customer_1.add(2.50, 2)

    # Start happy hour
    customer_1.billing_strategy = happy_hour_strategy
    customer_2.billing_strategy = happy_hour_strategy
    customer_1.add(3.40, 6)
    customer_2.add(3.10, 2)

    # End happy hour
    customer_1.billing_strategy = normal_strategy
    customer_2.billing_strategy = normal_strategy
    customer_1.add(3.10, 6)
    customer_2.add(3.10, 2)

    # Print the bills;
    print(customer_1)
    print(customer_2)

if __name__ == "__main__":
    main()

C++ Reference Implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#include <iostream.h>
#include <fstream.h>
#include <string.h>

class Strategy;

class Context
{
  public:
    enum StrategyType
    {
        Dummy, LeftFormating, RightFormating, CenterFormating
    };
    Context()
    {
        strategy_ = NULL;
    }
    void setStrategy(int type, int width);
    void format_operation();
  private:
    Strategy *strategy_;
};

class Strategy
{
  public:
    Strategy(int width): width_(width){}
    void pre_formating_algorithm()
    {
        char line[80], word[30];
        ifstream inFile("quote.txt", ios::in);
        line[0] = '\0';

        inFile >> word;
        strcat(line, word);
        while (inFile >> word)
        {
            if (strlen(line) + strlen(word) + 1 > width_)
              formating_algorithm(line);
            else
              strcat(line, " ");
            strcat(line, word);
        }
        formating_algorithm(line);
    }
  protected:
    int width_;
  private:
    virtual void formating_algorithm(char *line) = 0;
};

class LeftStrategy: public Strategy
{
  public:
    LeftStrategy(int width): Strategy(width){}
  private:
     /* virtual */void formating_algorithm(char *line)
    {
        cout << line << endl;
        line[0] = '\0';
    }
};

class RightStrategy: public Strategy
{
  public:
    RightStrategy(int width): Strategy(width){}
  private:
     /* virtual */void formating_algorithm(char *line)
    {
        char buf[80];
        int offset = width_ - strlen(line);
        memset(buf, ' ', 80);
        strcpy(&(buf[offset]), line);
        cout << buf << endl;
        line[0] = '\0';
    }
};

class CenterStrategy: public Strategy
{
  public:
    CenterStrategy(int width): Strategy(width){}
  private:
     /* virtual */void formating_algorithm(char *line)
    {
        char buf[80];
        int offset = (width_ - strlen(line)) / 2;
        memset(buf, ' ', 80);
        strcpy(&(buf[offset]), line);
        cout << buf << endl;
        line[0] = '\0';
    }
};

void Context::setStrategy(int type, int width)
{
  delete strategy_;
  if (type == LeftFormating)
    strategy_ = new LeftStrategy(width);
  else if (type == RightFormating)
    strategy_ = new RightStrategy(width);
  else if (type == CenterFormating)
    strategy_ = new CenterStrategy(width);
}

void Context::format_operation()
{
  strategy_->pre_formating_algorithm();
}

int main()
{
  Context test;
  int answer, width;
  cout << "Exit(0) LeftFormating(1) RightFormating(2) CenterFormating(3): ";
  cin >> answer;
  while (answer)
  {
    cout << "Width: ";
    cin >> width;
    test.setStrategy(answer, width);
    test.format_operation();
    cout << "Exit(0) LeftFormating(1) RightFormating(2) CenterFormating(3): ";
    cin >> answer;
  }
  return 0;
}

Output
  Exit(0) LeftFormating(1) RightFormating(2) CenterFormating(3): 2
  Width: 75
  Exit(0) LeftFormating(1) RightFormating(2) CenterFormating(3): 3
  Width: 75
References: