Thu 06 December 2018
Chain of Responsibility
Easily one of my favourite patterns from the gang of four is the chain of responsibility pattern. It aims to avoid coupling the object that sends a request to the handlers of the request. This is especially useful if the structure to our handlers follows a sense of hierarchy.
I've had very few opportunities to implement design patterns, but gaming has really helped me to envision a context where several designs can be applied as handy solutions to managing complexity.
So we will apply the chain of responsibility to a situation where we have gathered four of the most powerful wizards in a room, each of whom will test their power by casting a spell.
For this example we have our wizards as the objects which send requests.
class Wizard:
"""Create a wizard."""
def __init__(self, name: str, intelligence: int):
#: Identify the wizard by name
self.name = name
#: Intelligence as a proxy of a wizard's power.
self.intelligence = intelligence
def cast_spell(self, spell: Spell):
"""Have the wizard cast a spell."""
print(f"{self.name} casts {spell.name} spell.")
spell.cast(self)
We will make each behaviour of the spell, which is determined by the power of the wizard casting it, as an object handling a request. If the wizard does not meet the handler's requirements it passes the request on, to it's successor. This is where we have applied a sense of hierarchy to the handler.
To begin we have an Abstract Handler:
from abc import ABC
from abc import abstractmethod
class AbstractHandler(ABC):
def __init__(self, successor=None):
self._successor = successor
def handle(self, creature):
reaction = self._handle(creature)
if not reaction:
self._successor.handle(create)
@abstractmethod
def _handle(self, spell):
raise NotImplementedError("Must provide implementation in subclass.")
Now we can define our handler hierarchy:
class LowPowerFireSpell(AbstractHandler):
def _handle(self, creature):
if creature.intelligence < 10:
print("Spell backfires.")
return True
class MediumPowerFireSpell(AbstractHandler):
def _handle(self, creature):
if creature.intelligence < 20:
print("Small fire ball is cast.")
return True
class HighPowerFireSpell(AbstractHandler):
def _handle(self, creature):
if creature.intelligence < 30:
print("A fire ball blazes across the room")
return True
class GodlikePowerFireSpell(AbstractHandler):
def _handle(self, creature):
print("A Massive column of fire burns through the room!")
return True
Finally a single object to identify the spell chain.
class Spell:
def __init__(self, name: str):
#: Identifying the spell by a name
self.name = name
#: How the spell behaves at differing levels of user's power
self.chain = LowPowerFireSpell(
MediumPowerFireSpell(
HighPowerFireSpell(
GodlikePowerFireSpell()
)
)
)
def cast(self, creature):
self.chain.handle(creature)
To test their skill, we have a spell which casts a "Fire Ball". We have the following wizards present:
if __name__ == '__main__':
fire_spell = Spell('Fire Ball')
merlin = Wizard("Merlin", 8)
albus = Wizard("Albus", 18)
howl = Wizard("Howl", 28)
gandalf = Wizard("Gandalf", 38)
They are all gathered in a room, and take turns casting the same spell.
room = [merlin, albus, howl, gandalf]
for wizard in room:
wizard.cast_spell(fire_spell)
print("")
To which we should see the spell behave according to their intelligence:
(myenv) pc-name ~ $ python script.py
Merlin casts Fire Ball spell.
Spell backfires.
Albus casts Fire Ball spell.
Small fire ball is cast.
Howl casts Fire Ball spell.
A fire ball blazes across the room
Gandalf casts Fire Ball spell.
A Massive column of fire burns through the room!
(myenv) pc-name ~ $