One of the most common requirement which arises in software development is:
- We need to complete some medium to large size task.
- The way we complete some sub-tasks may vary, but the overall task should remain constant.
- We should be able to dynamically change the way some of those smaller tasks are carried out if needed.
If we do not think about writing with clean code and code maintainabilitiy in mind, we might just implement everything from scratch for each use case and copy + paste the common code in each place. However, a much simpler and more maintainable solution is to use the Strategy Pattern.
Strategy Pattern
This pattern lets us write code in a way where we can separate common / orchestration functionality from context / strategy specific functionality. We will use one class which will be the "primary" class which handles the overall task, then one or more "strategy" classes which handle smaller tasks. Strategies will differ based on the specific requirements in which that strategy will be used. This pattern can be implemented as follows:
- Determine which functionalities are common in all instances / contexts and which ones will vary based on context.
- For each type of functionality which will vary, create a strategy interface which outlines what that functionality should do.
- Create a class to implement each strategy interface for each context / use case.
- Provide a way to inject the strategy / strategies into the "primary" class. You can use a constructor parameter if it doesn't need to change after instantiation, or a setter property if it does.
In this diagram, we are using the example of logging. We have identified different types of cases / contexts for logging: error logging, transaction logging, and console logging. For the purposes of this example, we will use separate strategies for console logging, error logging, and transaction logging, which should all dispatch a log in some manner.
In the Logger class, which implements iLogger, we have a public method "recordLog", a private method "validateLog", and a private property "dispatchStrategy". The property dispatchStrategy will hold a reference to whichever type of strategy we will need depending upon the context of the Logger. For this example, we can assume the property is set by constructor dependency injection. When it is time to record the log, the Logger class will perform some validation to the incomming log object via "validateLog", and if the validation passes, it will call upon its "dispatchStrategy" to invoke its "dispatchLog" method.
Note that the Logger itself does not have any implementation for dispatching logs itself. A strategy must be provided for the functionality to work; otherwise some type of null related exception will occur.
This flow can be summarized as follows:
- Call the appropriate instance of a Logger class to record a log.
- Validate the incoming log is valid against some criteria and only proceed if valid (Logger instance handles this).
- Dispatch the log appropriately based on the context (strategy class handles this).
Strategy Pattern Implementation
The generic version of the strategy pattern implementation is as follows:
- Create a "primary" class which encapsulates all common logic. In the example above, the "primary" class was the Logger. This primary class will handle any common state, order in which operations are executed, and all other common concerns.
- Create a Strategy for each specific context. For each varying type of behavior, create a strategy interface + implementations. Note that more than one strategy type can be used within a single "primary" class, though using too many may indicate that the primary class is growing too large.
- Determine which strategy implementation(s) to use based on context. The class to use might be known at design time, or it may need to be selected dynamically at run time based on some set of circumstances or configurations.
In a more formal sense, this pattern allows you to select the algorithm, procedure, routine, etc. that will be used to perform some task dynamically.
Further Reading
This design pattern is one of the original design patterns by the Gang of Four, a nickname given to the authors of the original book on the subject called Patterns, Elements of Reusable Object-Oriented Software. The book introduces the concept of design patterns, then enumerates many of the design patterns which are now standard and well-known, such as:
- Template Method Pattern
- Strategy Pattern
- Factory Pattern
- Abstract Factory Pattern
- Builder Pattern
The book splits different types of patterns into different categories. When multiple patterns overlap in functionality and/or use cases, they compare and contrast the two accordingly. The book can be read in its entirety, or the reader can skip around to find specifically what they are looking for.