Strategy decorator
This article publishes a draft of a previously unreleased object-oriented design pattern |
See also UML diagrams of the Strategy Decorator pattern on the next pages
Introduction
The Strategy Decorator design pattern is a behavioral software design pattern that enables selecting an algorithm at runtime. It lets the algorithm vary independently from the clients using it. The Strategy Decorator pattern makes it possible to add or alter behavior without changing existing implementations and it makes it possible to add or alter behavior of an individual interface operation dynamically at runtime in a multilayered and arbitrary combination.
Context
We have to implement a complex business process or we have to heavily change the implementation of a complex business process. During that process we have to choose between alternative behaviors. Typically, we have to determine by parameters which behavior shall be choosen to dispatch the process accordingly. The way we implement the dispatch shall not restrict the types or the number of the parameters we could use to determine a behavior.
The parameters may or may not relate to each other hierarchically. The behaviors which could possibly be choosen may or may not relate to each other hierarchically, no matter if the parameters relate to each other hierarchically. For instance the same behavior might be choosen by different sets of parameter values and even by sets of parameters with different parameter types. The way we implement the dispatch shall facilitate to implement new behaviors, to add implemented behaviors to the process and to remove them from the process.
We shall split the implementation of the behaviors into small work packages in order to facilitate a parallelization of the work.
Problem
With traditional code using conditional statements like if-then-else, the code will become hard to understand as well as hard to change.
Conditions might be too complex and inconsistent. Some implementations of behavior might be duplicated.
We could use the Strategy Design Pattern, but for each change of the strategy interface, for instance when adding an operation, we would have to adapt all strategies and the single dispatch nature of the Strategy Design Pattern somewhat restricts the number of parameters we can use for the dispatch. When we dispatch by more than one parameter the strategies often become cluttered and hard to understand.
To support several parameters which relate to each other hierarchically, we could build a hierarchy of inherited behavior, but we would have to adapt the hierarchy on each change of the strategy interface and to choose the same behavior for different sets of parameter values or for different parameter types, we might have to duplicate some implementations.
A strategy implements all operations of the strategy interface and can become a quite huge piece of code. Implementations of operations are not decoupled and hence it is possibly not easy to parallelize the work.
Solution
- We decompose strategies into their operations, so that each strategy operation can be implemented in a separate class.
- We make it possible to chain implementations of the same strategy interface in any combination and in any order.
- We make it possible to add and use strategy operations without an implementation for the respective operation.
- We implement a generic default behavior once and for all invocations of strategy operations where no other behavior applies.
Class Diagram I
Class Diagram I Benefits
This design allows to
- select algorithms in any combination and in any order, separately for any operation of the strategy interface and determined by any number, any type and any combination of parameters.
- apply behavior hierarchically where it is needed, with an unlimited depth of the hierarchy, for instance if the parameters relate to each other hierarchically.
- to choose the same behavior for different sets of parameter values or for different parameter types, without having to duplicate the implementation of that behavior.
- to parallelize the work easily, since the implementations will be much smaller pieces of code and decoupled from each other.
- add operations to the strategy interface without having to add or change any strategy or decorator. Just the forwarding to the decorated strategy has to be added.
- automatically apply a generic default behavior where no other behavior has been added. A strategy operation can be used even without any decorator for that operation.
- generate a proxy for each decorated strategy in order to apply AOP behavior for cross-cutting concerns like logging or authorization.
Class Diagram I Principles
The Strategy Decorator design pattern adheres to the SOLID principles:
- Single-Responsibility Principle (SRP)
- Open-Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
As well as to the following principles:
- Composition over Inheritance (CoI)
- Don‘t Repeat Yourself (DRY)
Class Diagram I Drawbacks
Remember the saying
There ain't no such thing as a free lunch
This design has the following potential drawbacks:
- The behavior of an application might be less comprehensible, if decorators are added to the strategy anywhere in the middle of the business code instead of adding them at specific places.
- To prevent this, introduce a separate interface for the strategy decorators that extends the strategy. This decouples the usage of the strategy from the decoration of the strategy. See next diagram.
- For each operation that returns a value it has to be clear how a decorator shall handle the value from the decorated strategy to be consistent with other decorators.
- To achieve this, comment the operations in the strategy interface where needed, e.g. in Java use Javadocs or custom Annotations for that. For instance specify if the value from the decorated strategy should be dropped or used for further calculations.
- Moreover, it should be avoided that the behavior of a decorator for a certain operation logically depends on the behavior of another decorator. In cases where it cannot be avoided, the decorator should only implement operations depending on the same behavior.
- The code of a long chain of decorators might not be formatted nicely by the IDE out of the box, but on the other hand it should be simple to circumvent that issue.
- To circumvent that issue, turn off the formatting for these lines and format manually. Popular IDE like Eclipse and IntelliJ support tags for that („// @formatter:off“, „// @formatter:on“) or turn off the formatting for each line by a following double slash („//“).
Class Diagram II with decorator interface
Class Diagram II Benefits
This design prevents that developers are tempted to add decorators to the strategy anywhere in the middle of the business code. Instead, decorators will be added to the strategy at specific places only, typically at the beginning of the process. Thus, we know where in the code we can analyze the behavior that has been added with strategy decorators.
Class Diagram III with specialized interfaces
Class Diagram III Benefits
- This design allows a specialized context to use a specialized interface of the strategy.
- The strategy can be passed from one or several specialized contexts to another (shared) context with the generalized strategy interface.
- New specialized interfaces with new operations can be added without having to add or change any strategy or decorator. Just the forwarding to the decorated strategy has to be added.
- A separate interface for the strategy decorators is already in place and can be used to extend all specialized interfaces of the strategy, hence we do not need to add another interface for that.
Class Diagram IV example 1
Related Design Patterns
The Strategy Decorator design pattern is closely related to the following design patterns:
- Decorator
- Strategy
- Proxy
And is also related to the following design patterns:
- Chain-of-responsibility
- Composite
- Delegation
- Bridge
- Adapter
- Facade
- Command.
Other usages
The strategy decorators may or may not have a state. In most cases strategy decorators will be stateless, since the main purpose of this pattern is to add or alter behavior. Stateless strategy decorators can be injected into a context using the Dependency Injection
design pattern, instead of passing the strategy to the context as an argument.
Stateful strategy decorators can be used for local calculations based on their state, and they can mutate their state during a business process, to create a chain of calculations e.g. Stateful strategy decorators can be used as well in a command pattern style holding the state that is needed to perform a certain action, to create a chain of commands e.g.
Interface evolution
Given, a team works on a context using a new strategy interface, but another team working in parallel is responsible for the implementation of that strategy interface. And the operations needed for that strategy interface are not yet fully specified and predictable, but both teams cannot wait for the specification due to their deadlines.
Then, in order to parallelize work and to avoid issues with the collaboration of both teams, we could decouple the work packages of both teams using the Strategy Decorator pattern. With the Strategy Decorator pattern it is not required to specify the interface upfront. Instead we can develop the interface gradually, driven by the context that uses the interface, without any interference between the teams.
Team one, working on the context, can add operations to the strategy interface as needed for the context, along with the forwarding to the decorated strategy. Team two, responsible for the strategy implementation, can implement and add decorators for the operations added by team one anytime later.
If an operation becomes obsolete for the context, team one marks the operation as deprecated. Team two can remove implementations anytime later.
If the signature of an operation has to be changed for the context, team one marks the operation as deprecated and creates a new operation with the required signature. Team two can adapt the implementations anytime later.
Nesting
The creation of a decorator can be nested within another decorator and hence the creation of a strategy can be nested within another strategy . The nesting decorator / strategy is also called the outer decorator / strategy and the nested decorators / strategies are also called the inner decorators / strategies. This should not be confused with the parent-child relationship of chained decorators where the child-decorator is passed to the parent-decorator as an argument.
A context can get an inner strategy from an outer strategy and may pass it to subordinated contexts. To realize main-sub process relationships with strategies dedicated strategies can be created for and passed to each main- or subprocess.
Inner strategies can be decorated within the outer strategy before returning them to a context. Thus, we can create a dedicated strategy in one place and over decorate that strategy in another place. A team may work on a subprocess and create provisional decorator dummies for their inner strategy while another team working in parallel over decorates them with the real decorators.
Given, a team one works on a subprocess using an inner strategy passed to that subprocess. And a team two is responsible for the main process and the outer strategy. The main process gets the inner strategy from the outer strategy and passes it to the subprocess.
Then, team one creates the inner strategy in a decorator of the outer strategy and may or may not add provisional decorator dummies to their inner strategy as needed for the time being. Team two can independently from team one over decorate the inner strategy with the real decorators they implement. Hence, team two can replace provisional behavior from team one with the real behavior little by little, without any interference between the teams.
More coming soon.
About the creator & author