Strategy Design Pattern in Python
What is it?
The Strategy Design Pattern provides multiple interchangeable algorithms to the client. The client is offered a Strategy class with a default behavior. And when there is a need to dynamically replace the default behavior, the client is provided another variation of the Strategy class.
Consider a situation where you are designing a simple application that manipulates a file. The application reads the entire content of the file in one go, manipulates it and writes the manipulated text to another file. You tested the application, it works fine. You are about to ship it to your client, when he tells you that it is uncommon for him to deal with files having gigantic size, but it does happen. You think to yourself, this might cause an issue for the processing unit if the application reads the whole file at a single instant. So, you think of modifying the existing code to incorporate this requirement. Essentially, your application is supposed to have a default behavior of reading the file in one go, with the ability to read it in chunks if the size is over a certain threshold limit. The Strategy Pattern will do the deed.
The Strategy Pattern is implemented in Python by dynamically replacing the contents of a method defined inside a class with the contents of a function defined outside the class. There are 3 ways to achieve this, you can read more about them here. We will be using the method MethodType of module types to actualize our code.
It is classified under Behavioural Design Patterns as it provides an industry-wide accepted method to handle communication between objects.
Why the need for it: Problem Statement
The need for Strategy is felt when the behavior of an object needs to changed dynamically.
Pseudo Code
class Strategy: '''Strategy Class: houses default strategy; provides mechanism to replace the execute() method with a supplied method''' def __init__(strategyName = 'Default Strategy', replacementFunction=None): '''If strategyName is provided, sets strategy name to string provided; If a reference to new strategy function is provided, replace the execute() method with the supplied function.''' def execute(): '''Contains default strategy behavior. # Behavior of Strategy One def strategyOne(): '''Contains behavior of Strategy One.''' # Behavior of Strategy Two def strategyTwo(): '''Contains behavior of Strategy Two.''' ## USING THE ABOVE SETUP ## # Instantiate Default Strategy # Instantiate Strategy One # Instantiate Strategy Two # Execute different strategies by calling the execute() method
How to implement it
In addition to the above code, we will use the MethodType method of types module, as it not only replaces the body of the execute method with the replacement function, but also passes a reference of the current instance of the class i.e. self to the execute method. You can read about this method in detail here.
import sys import types # if construct to select the right amount of arguments to method MethodType # MethodType binds a function to an instance of a class, or to all instances of the class. # The function being bound either gets added in the class definition, or replaces the contents of a method defined in the class. # The MethodType takes two arguments in Python 3, first, the method to be bound, second, the instance to which the method is to be bound. # In Python 2, there is third positional argument, which seeks the class to which the instance passed as second argument, belongs. if sys.version_info[0] > 2: # Python 3 createBoundMethod = types.MethodType else: def createBoundMethod(func, obj): # Python 2 return types.MethodType(func, obj, obj.__class__) class Strategy: '''Strategy Class: houses default strategy; provides mechanism to replace the execute() method with a supplied method''' def __init__(self, strategyName = 'Default Strategy', replacementFunction=None): '''If strategyName is provided, sets strategy name to string provided; If a reference to new strategy function is provided, replace the execute() method with the supplied function.''' self.name = strategyName if replacementFunction: self.execute = createBoundMethod(replacementFunction, self) def execute(self): '''Contains default strategy behavior. If a reference to a new strategy function is provided, then the body of this function gets replaced by body of supplied method''' print("Executing {}...".format(self.name)) # Behavior of Strategy One def strategyOne(self): '''Contains behavior of Strategy One.''' print("Executing {}...".format(self.name)) # Behavior of Strategy Two def strategyTwo(self): '''Contains behavior of Strategy Two.''' print("Executing {}...".format(self.name)) ## USING THE ABOVE SETUP ## # Instantiating Default Strategy defaultStrategy = Strategy() # Instantiating Strategy One strategyONE = Strategy('Strategy One', strategyOne) # Instantiating Strategy Two strategyTWO = Strategy('Strategy Two', strategyTwo) # Executing different strategies defaultStrategy.execute() strategyONE.execute() strategyTWO.execute() ### OUTPUT ### Executing Default Strategy... Executing Strategy One... Executing Strategy Two...
Walkthrough of implementation
- First, we instantiate the default strategy.
- Then, we instantiate each of our variant strategies, passing the variant strategy functions as arguments to the Strategy class.
- Finally, we execute each of our strategies calling the execute() method of each strategy instance.
Implementing the Scenario
Consider the same situation I described to you in the introduction to Strategy Pattern, where you are designing a simple application that manipulates a file. The application reads the entire content of the file in one go, manipulates it and writes the manipulated text to another file. You tested the application, it works fine. You are about to ship it to your client, when he tells you that it is uncommon for him to deal with files having gigantic size, but it does happen. You think to yourself, this might cause an issue for the processing unit if the application reads the whole file at a single instant. So, you think of modifying the existing code to incorporate this requirement.
Essentially, your application is supposed to have a default behavior of reading the file in one go (Default Strategy), with the ability to read it in chunks if the size is over a certain threshold limit (Strategy One). For the sake of simplicity, let's say this threshold is 10 bytes, and the chunk size is 20 bytes. Code for actualizing the above scenario would look something like this.
import types import sys if sys.version_info[0] > 2: # Python 3 createBoundMethod = types.MethodType else: def createBoundMethod(func, obj): # Python 2 return types.MethodType(func, obj, obj.__class__) class StrategicFileHandler: '''Houses default strategy. Provides mechanism to replace Default Strategy behavior with behavior of Strategy One.''' def __init__(self, fileHandler, fileHandlingStrategy=None): '''Accepts a fileHandler and stores it locally. If a reference to new strategy function is provided, replace the handleFile() method with the supplied function..''' self.fileHandler = fileHandler if fileHandlingStrategy: self.handleFile = createBoundMethod(fileHandlingStrategy, self) def handleFile(self): '''Default strategy: employed when length of file contents is less than 10 bytes.''' print("File contents less than 10 bytes. Reading in one go....") print(self.fileHandler.read()) # further code to manipulate the contents def strategyOneMethod(self): '''Strategy One: employed when file is large.''' print("File contents more than 10 bytes. Reading first 20 bytes...") print(self.fileHandler.read(20)) # further code to manipulate the contents; could use a buffer to read next installments of 20 bytes till end of file. ## USING THE ABOVE SETUP ## # File to be handled strategically # fileToBeHandled = open('fileToBeHandled.txt') # Instantiating the two strategies defaultStrategy = StrategicFileHandler(fileToBeHandled) strategyOne = StrategicFileHandler(fileToBeHandled, strategyOneMethod) # Invoking the correct strategy based on a condition if len(fileToBeHandled.read()) < 10: # The read() method in the testing condition places the file cursor at the end of the file. fileToBeHandled.seek(0) # Resetting file cursor to 0 so that we can read from the beginning. defaultStrategy.handleFile() else: fileToBeHandled.seek(0) strategyOne.handleFile() ### OUTPUT when contents of fileToBeHandled.txt are 'abcde' ### File contents less than 10 bytes. Reading in one go.... abcde ### OUTPUT when contents of fileToBeHandled.txt are 'abcdefghijklmnopqrstuvwxyz' ### File contents more than 10 bytes. Reading first 20 bytes... abcdefghijklmnopqrst
- Creational Patterns
- Factory
- Abstract Factory
- Prototype
- Singleton
- Builder
- Architectural Pattern
- Model View Controller (MVC)