The Clash of Template and Delegate Patterns
Back in my delegate decorator article, I mentioned some weaknesses of the delegate pattern as a substitute to inheritance. The decorator solved one of those problems, but the other is still a problem. The problem comes when using something akin to the template pattern.
The Problem
For example, if you have this class:
class TemplateUser: def intermediate_step(self): ... def multi_step_operation(self): ... self.intermediate_step() ...
Then you try to create a delegating class like this:
class TemplateUserDelegator: def __init__(self, delegate): self.delegate = delegate def intermediate_step(self): ... self.delegate.intermediate_step() ... def multi_step_operation(self): ... self.delegate.multi_step_operation() ...
Unfortunately, when you run the line:
TemplateUserDelegator(TemplateUser()).multi_step_operation()
TemplateUserDelegator
‘s wrapper of intermediate_step()
doesn’t get called. Why is that? Because you’re asking delegate
to run multi_step_operation()
, which doesn’t have TemplateUserDelegator
‘s version of intermediate_step()
within.
Attempts At Solutions
We could explicitly call the delegator’s intermediate_step()
within its multi_step_operation()
, but that would result in calling delegate
‘s intermediate_step()
twice; once within the wrapped version and once within delegate
‘s multi_step_operation()
. There are some few cases where that could work.
What if TemplateUserDelegator
‘s intermediate_step()
only did its own work without delegating to delegate
‘s? Again, sometimes, that might work, but not usually. Often, it’s not quite the case of the pure template pattern, where the intermediate steps are “protected”. There are many times where the “intermediate step” can also be called on its own as a full step.
For example, for a collection, it could have an add()
method that adds one item to the collection and an add_all()
method that adds many items to the collection at once. Likely, the add_all()
method makes a call to add()
for each item it’s attempting to add. If you were to extend that collection with delegation and that extension does a transformation action to the item being added. What do you do then? About the only solution then is to completely reimplement add_all()
without delegating. That’s not very DRY, though, so we could really use something different.
Solution
The solution is to switch from the template pattern to the strategy pattern.
So we’d rewrite TemplateUser
like this instead.
class TemplateUser: def __init__(self, strategy): self.strategy = strategy def intermediate_step(self): self.strategy.intermediate_step(self) def multi_step_operation(self): ... self.intermediate_step() ...
Technically, since this strategy type only requires one method, it should be a callable instead and just called via self.strategy()
.
Note that self
is also passed into the strategy method. This is to give it access to TemplateUser
‘s fields if need be. This should actually be avoided in most cases. Most decoration/delegation is done only to the input parameters and return values. Giving access to fields increases coupling. It is simply shown for the sake of being an example possibility.
What if you can’t make changes to TemplateUser
? What then? Well, then you’ll actually have to use a little bit of inheritance to enable the use of composition.
class DelegatableTemplateUser(TemplateUser): def __init__(self, strategy): super().__init__(self) self.strategy = strategy def intermediate_step(self): self.strategy.intermediate_step(super().intermediate_step) def multi_step_operation(self): self.strategy.multi_step_operation(super().multi_step_operation)
You use the new subclass to delegate to the strategy objects you provide, which have all the steps methods with the same name and same set of parameters, except it also needs a parameter to accept the base method as well. This allows the strategy to call the delegated-to method when it needs to, or skip calling it, if it prefers (which is not a good idea, in most cases).
It can also take in self
if it needs to, which is not shown in this example. This has the same warnings as it did with the previous solution.
Note: do not accidentally call super()
‘s methods when passing them into the strategy calls.
Outro
So that’s how you can design the “template pattern” to be used by the delegation pattern. The second solution can be kind of a pain, but if it needs to be done, then it’s worth it.
To prevent some of the negative comments I’m going to get: yes, I know inheritance is good and useful at times, but the experts say to prefer composition over inheritance, and from what I’ve seen, I’m highly inclined to do that. I’m trying to help people get out of ruts where it looks like they CAN’T do the composition they want.
On another note, I won’t be posting for a little while now. I’m going to be working on writing up a (hopefully) comprehensive guide to python descriptors. Sadly, it won’t be posted right away either. I’m looking to have it be a paid publication. When it’s available, I’ll let you guys know.
Reference: | The Clash of Template and Delegate Patterns from our WCG partner Jacob Zimmerman at the Programming Ideas With Jake blog. |