- 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.
- 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.
- 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
- 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.
- 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.