본문 바로가기
Practical Python Design Patterns

Strategy Pattern

by 자동매매 2023. 3. 29.

CHAPTER Strategy Pattern

Move in silence, only speak when it s time to say Checkmate.

Unknown

From time to time, you might find yourself in a position where you want to switch between different ways of solving a problem. You essentially want to be able to pick a strategy at runtime and then run with it. Each strategy might have its own set of strengths and weaknesses. Suppose you want to reduce two values to a single value. Assuming these values are numeric values, you have a couple of options for reducing them.

As an example, consider using simple addition and subtraction as strategies for reducing the two numbers. Let s call them arg1 and arg2. A simple solution would be something like this:

def reducer(arg1, arg2, strategy=None):

if strategy == "addition":

print(arg1 + arg2)

elif strategy == "subtraction":

print(arg1 - arg2)

else:

print("Strategy not implemented...")

def main():

reducer(4, 6)

reducer(4, 6, "addition") reducer(4, 6, "subtraction")

if __name__ == "__main__":

main()

249

' Wessel Badenhorst 2017

W. Badenhorst, Practical Python Design Patterns, https://doi.org/10.1007/978-1-4842-2680-3_16
CHAPTER 17 STRATEGY PATTERN

This solution results in the following:

Strategy not implemented... 10

-2

This is what we want. Sadly, we suffer from the same problem we encountered in previous chapters, namely that whenever we want to add another strategy to the reducer, we have to add another elif statement to the function together with another block of code to handle that strategy. This is a surefire way to grow a sprawling if statement.

We would much prefer a more modular solution that would allow us to pass in new strategies on the fly without our having to alter the code that uses or executes the strategy.

As you have come to expect by now, we have a design pattern for that.

This design pattern is aptly named the strategy pattern because it allows us to write code that uses some strategy, to be selected at runtime, without knowing anything about the strategy other than that it follows some execution signature.

Once again, we turn to an object to help us solve this problem. We will also reach for the fact that Python treats functions as first-class citizens, which will make the implementation of this pattern much cleaner than the original implementation.

We will start with the traditional implementation of the strategy pattern.

class StrategyExecutor(object):

def __init__(self, strategy=None):

self.strategy = strategy

def execute(self, arg1, arg2):

if self.strategy is None:

print("Strategy not implemented...") else:

self.strategy.execute(arg1, arg2)

class AdditionStrategy(object): def execute(self, arg1, arg2):

print(arg1 + arg2)

class SubtractionStrategy(object): def execute(self, arg1, arg2):

print(arg1 - arg2)

250
CHAPTER STRATEGY PATTERN

def main():

no_strategy = StrategyExecutor()

addition_strategy = StrategyExecutor(AdditionStrategy()) subtraction_strategy = StrategyExecutor(SubtractionStrategy())

no_strategy.execute(4, 6) addition_strategy.execute(4, 6) subtraction_strategy.execute(4, 6)

if __name__ == "__main__":

main()

This again results in the required output:

Strategy not implemented... 10

-2

At least we dealt with the sprawling if statement as well as the need to update the executor function every time we add another strategy. This is a good step in the right direction. Our system is a little more decoupled, and each part of the program only deals with the part of the execution it is concerned with without its worrying about the other elements in the system.

In the traditional implementation, we made use of duck typing, as we have done many times in this book. Now, we will use another powerful Python tool for writing clean code using functions as if they were any other value. This means we can pass a function to the Executor class without first wrapping the function in a class of its own. This will not only greatly reduce the amount of code we have to write in the long run, but it will also make our code easier to read and easier to test since we can pass arguments to the functions and assert that they return the value we expect.

class StrategyExecutor(object):

def __init__(self, func=None): if func is not None: self.execute = func

def execute(self, *args):

print("Strategy not implemented...")

251
CHAPTER STRATEGY PATTERN

def strategy_addition(arg1, arg2):

print(arg1 + arg2)

def strategy_subtraction(arg1, arg2):

print(arg1 - arg2)

def main():

no_strategy = StrategyExecutor()

addition_strategy = StrategyExecutor(strategy_addition) subtraction_strategy = StrategyExecutor(strategy_subtraction)

no_strategy.execute(4, 6) addition_strategy.execute(4, 6) subtraction_strategy.execute(4, 6)

if __name__ == "__main__":

main()

Again, we get the required result:

Strategy not implemented... 10

-2

Since we are already passing around functions, we might as well take advantage of having first-class functions and abandon the Executor object in our implementation, leaving us with a very elegant solution to the dynamic-strategy problem.

def executor(arg1, arg2, func=None):

if func is None:

print("Strategy not implemented...") else:

func(arg1, arg2)

def strategy_addition(arg1, arg2):

print(arg1 + arg2)

def strategy_subtraction(arg1, arg2):

print(arg1 - arg2)

252
CHAPTER STRATEGY PATTERN

def main():

executor(4, 6)

executor(4, 6, strategy_addition) executor(4, 6, strategy_subtraction)

if __name__ == "__main__":

main()

As before, you can see that the output matches the requirement:

Strategy not implemented... 10

-2

We created a function that could take a pair of arguments and a strategy to reduce them at runtime. A multiplication or division strategy, or any binary operation to reduce the two values to a single value, can be defined as a function and passed to the reducer. We do not run the risk of a sprawling if growing in our executor and causing code rot to appear.

One thing that is a bit bothersome about the preceding code snippet is the else statement in the executor. We know that we will usually not be printing text in the terminal as the result of something happening inside our program. It is way more likely that the code will return some value. Since the print statements are used to demonstrate the concepts we are dealing with in the strategy pattern in a tangible manner, I will implement the strategy pattern, using a print statement in the main function to simply print out the result of the executor. This will allow us to use an early return to get rid of the dangling else, and thus clean up our code even more.

The cleaned-up version of our code now looks something like this:

def executor(arg1, arg2, func=None):

if func is None:

return "Strategy not implemented..."

return func(arg1, arg2)

def strategy_addition(arg1, arg2):

return arg1 + arg2

253
CHAPTER STRATEGY PATTERN

def strategy_subtraction(arg1, arg2):

return arg1 - arg2

def main():

print(executor(4, 6))

print(executor(4, 6, strategy_addition)) print(executor(4, 6, strategy_subtraction))

if __name__ == "__main__":

main()

Once again, we test that the code results in the output we saw throughout this

chapter:

Strategy not implemented... 10

-2

Indeed it does. Now we have a clean, clear, and simple way to implement different strategies in the same context. The executor function now also uses an early return to discard invalid states, which makes the function read more easily in terms of the actual execution of the optimal case happening on a single level.

In the real world, you might look at using different strategies for evaluating the stock market and making purchasing decisions; alternatively, you might look at different pathfinding techniques and switch strategies there.

Parting Shots

The broken windows theory works just as well in code as in real life. Never tolerate broken windows in your code. Broken windows, in case you were wondering, are those pieces of code that you know are not right and will not be easy to maintain or extend. It is the type of code you really feel like slapping a TODO comment on, that you know you need to come back and fix but never will. To become a better coder, you need to take responsibility for the state of the codebase. After you have had it in your care, it should be better, cleaner, and more maintainable than before. Next time you feel the temptation to leave a to-do as a booby trap for the poor developer who passes this way after you, roll

254
CHAPTER STRATEGY PATTERN

up your sleeves and knock out that fix you had in mind. The next poor coder who has to work on this code might just be you, and then you will thank the coder who came before and cleaned things up a little.

Exercises

See if you can implement a maze generator that will print a maze

using # and to represent walls and paths, respectively. Before generating the maze, have the user pick one of three strategies. Use the strategy pattern to have the generator strategy passed to the maze generator. en use that strategy to generate the maze.

255

'Practical Python Design Patterns' 카테고리의 다른 글

Visitor Pattern  (0) 2023.03.29
Template Method Pattern  (0) 2023.03.29
State Pattern  (0) 2023.03.29
Observer Pattern  (0) 2023.03.29
Iterator Pattern  (0) 2023.03.29

댓글