Strategy (#22)

* add documentation

* add code example
This commit is contained in:
Ruidy 2020-10-09 12:01:22 +02:00 committed by GitHub
parent 1694f453ee
commit 03cb570b65
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 137 additions and 0 deletions

View file

@ -46,6 +46,7 @@ and divided into three groups.
- [Memento](behavioral/memento/README.md)
- [Observer](behavioral/observer/README.md)
- [State](behavioral/state/README.md)
- [Strategy](behavioral/strategy/README.md)
## Resources

View file

@ -11,3 +11,4 @@ Behavioral design patterns are concerned with algorithms and the assignment of r
- [Memento](memento/README.md)
- [Observer](observer/README.md)
- [State](state/README.md)
- [Strategy](strategy/README.md)

View file

@ -0,0 +1,46 @@
# Strategy
Strategy is a behavioral design pattern that lets you define a family of algorithms, put each of them into a separate class, and make their objects interchangeable.
## Problem
Imagine a navigation app for casual travelers. The app helped users quickly orient themselves in any city.
One of the most requested features for the app was automatic route planning. A user should be able to enter an address and see the fastest route to that destination displayed on the map.
The first version of the app could only build the routes over roads. People who traveled by car were bursting with joy. But apparently, not everybody likes to drive on their vacation. So with the next update, you added an option to build walking routes. Right after that, you added another option to let people use public transport in their routes.
However, that was only the beginning. Later you planned to add route building for cyclists. And even later, another option for building routes through all of a citys tourist attractions.
While from a business perspective the app was a success, the technical part caused you many headaches. Each time you added a new routing algorithm, the main class of the navigator doubled in size. At some point, the beast became too hard to maintain.
Any change to one of the algorithms, whether it was a simple bug fix or a slight adjustment of the street score, affected the whole class, increasing the chance of creating an error in already-working code.
In addition, teamwork became inefficient. Your teammates, who had been hired right after the successful release, complain that they spend too much time resolving merge conflicts. Implementing a new feature requires you to change the same huge class, conflicting with the code produced by other people.
## Solution
The Strategy pattern suggests that you take a class that does something specific in a lot of different ways and extract all of these algorithms into separate classes called strategies.
The original class, called context, must have a field for storing a reference to one of the strategies. The context delegates the work to a linked strategy object instead of executing it on its own.
The context isnt responsible for selecting an appropriate algorithm for the job. Instead, the client passes the desired strategy to the context. In fact, the context doesn't know much about strategies. It works with all strategies through the same generic interface, which only exposes a single method for triggering the algorithm encapsulated within the selected strategy.
This way the context becomes independent of concrete strategies, so you can add new algorithms or modify existing ones without changing the code of the context or other strategies.
In our navigation app, each routing algorithm can be extracted to its own class with a single `buildRoute` method. The method accepts an origin and destination and returns a collection of the routes checkpoints.
Even though given the same arguments, each routing class might build a different route, the main navigator class doesn't really care which algorithm is selected since its primary job is to render a set of checkpoints on the map. The class has a method for switching the active routing strategy, so its clients, such as the buttons in the user interface, can replace the currently selected routing behavior with another one.
## How to Implement
1. In the context class, identify an algorithm thats prone to frequent changes. It may also be a massive conditional that selects and executes a variant of the same algorithm at runtime.
1. Declare the strategy interface common to all variants of the algorithm.
1. One by one, extract all algorithms into their own classes. They should all implement the strategy interface.
1. In the context class, add a field for storing a reference to a strategy object. Provide a setter for replacing values of that field. The context should work with the strategy object only via the strategy interface. The context may define an interface which lets the strategy access its data.
1. Clients of the context must associate it with a suitable strategy that matches the way they expect the context to perform its primary job.

View file

@ -0,0 +1,4 @@
"""
Lets you define a family of algorithms, put each of them into a separate class,
and make their objects interchangeable.
"""

View file

@ -0,0 +1,15 @@
"""
Concrete Strategies implement the algorithm while following the base Strategy interface. The interface makes them
interchangeable in the Context.
"""
from behavioral.strategy.strategy import Strategy
class ConcreteStrategyA(Strategy):
def do_algorithm(self, data: list) -> list:
return sorted(data)
class ConcreteStrategyB(Strategy):
def do_algorithm(self, data: list) -> list:
return reversed(sorted(data))

View file

@ -0,0 +1,40 @@
from behavioral.strategy.strategy import Strategy
class Context:
"""
The Context defines the interface of interest to clients.
"""
def __init__(self, strategy: Strategy):
"""
Usually, the Context accepts a strategy through the constructor, but also provides a setter to change it at
runtime.
"""
self._strategy = strategy
@property
def strategy(self):
"""
The Context maintains a reference to one of the Strategy objects. The
Context does not know the concrete class of a strategy. It should work
with all strategies via the Strategy interface.
"""
return self._strategy
@strategy.setter
def strategy(self, strategy: Strategy):
"""
Usually, the Context allows replacing a Strategy object at runtime.
"""
self._strategy = strategy
def do_some_business_logic(self) -> None:
"""
The Context delegates some work to the Strategy object instead of implementing multiple versions of the
algorithm on its own.
"""
print("Context: Sorting data using the strategy (not sure how it'll do it)")
result = self._strategy.do_algorithm(["a", "b", "c", "d", "e"])
print(",".join(result))

View file

@ -0,0 +1,15 @@
"""
The client code picks a concrete strategy and passes it to the context.
The client should be aware of the differences between strategies in order to make the right choice.
"""
from behavioral.strategy.concrete_strategies import ConcreteStrategyA, ConcreteStrategyB
from behavioral.strategy.context import Context
context = Context(ConcreteStrategyA())
print("Client: Strategy is set to normal sorting.")
context.do_some_business_logic()
print()
print("Client: Strategy is set to reverse sorting.")
context.strategy = ConcreteStrategyB()
context.do_some_business_logic()

View file

@ -0,0 +1,15 @@
from abc import ABC, abstractmethod
class Strategy(ABC):
"""
The Strategy interface declares operations common to all supported versions
of some algorithm.
The Context uses this interface to call the algorithm defined by Concrete
Strategies.
"""
@abstractmethod
def do_algorithm(self, data: list):
pass