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 ~ $

Implementation can be found here