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.
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‘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
Attempts At Solutions
We could explicitly call the delegator’s
intermediate_step() within its
multi_step_operation(), but that would result in calling
intermediate_step() twice; once within the wrapped version and once within
multi_step_operation(). There are some few cases where that could work.
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.
The solution is to switch from the template pattern to the strategy pattern. I’ve already talked about this in another article.
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 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.
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.