8. Behavioral Patterns

  • Chain of Responsibility Design Pattern
  • Command Design Pattern
  • Iterator Design Pattern
  • Mediator Design Pattern
  • Observer Design Pattern
  • State Design Pattern
  • Strategy Design Pattern
  • Template Method Design Pattern
  • Visitor Design Pattern

8.1 Chain of Responsibility Design Pattern

Chain of Responsibility is a behavioral design pattern that lets you pass requests along a chain of handlers.

Upon receiving a request, each handler decides either to process the request or to pass it to the next handler in the chain.

# Realtime Usage
* ATM, Vending Machine
* Design Logger

Example - 1

https://sbcode.net/python/chain_of_responsibility/

import sys

class IDispenser:
    """Methods to implement"""
    def next_successor(self, successor):
        """Set the next handler in the chain"""
        pass
    def handle(self, amount):
        """Handle the events(dispensing of notes)"""
        pass

class Dispenser(IDispenser):
    """ Added this to remove common function """
    def __init__(self):
        self._successor = None
    
    def dispense_amount(self, amount, note):
        if amount >= note:
            num = amount // note
            remainder = amount % note
            print(f"Dispensing {num} Rs {note} note - from {type(self).__name__}")
            # OR self.__class__.__name__
            if remainder != 0:
                self._successor.handle(remainder)
        else:
            self._successor.handle(amount)

class Dispenser10(Dispenser):
    """Dispenses Rs 10s if applicable, otherwise continues to next successor"""
    def next_successor(self, successor):
        self._successor = successor
    def handle(self, amount):
        self.dispense_amount(amount, 10)


class Dispenser20(Dispenser):
    """Dispenses Rs 20s if applicable, otherwise continues to next successor"""
    def next_successor(self, successor):
        self._successor = successor
    def handle(self, amount):
        self.dispense_amount(amount, 20)


class Dispenser50(Dispenser):
    """Dispenses Rs 50s if applicable, otherwise continues to next successor"""
    def next_successor(self, successor):
        self._successor = successor
    def handle(self, amount):
        self.dispense_amount(amount, 50)

class ATMDispenserChain:
    """The Chain Client"""
    def __init__(self):
        # initializing the successors chain
        self.chain1 = Dispenser50()
        self.chain2 = Dispenser20()
        self.chain3 = Dispenser10()
        # Setting a default successor chain that will process the 50s first,
        # the 20s second and the 10s last.
        # The successor chain will be recalculated dynamically at runtime.
        self.chain1.next_successor(self.chain2)
        self.chain2.next_successor(self.chain3)
        
if __name__ =='__main__':
    AMOUNT = int(input("Enter amount to withdrawal : "))
    if AMOUNT < 10 or AMOUNT % 10 != 0:
        print("Amount should be positive and in multiple of 10s.")
        sys.exit()
        
    ATM = ATMDispenserChain()
    ATM.chain1.handle(AMOUNT) # process the request
    print("Now go spoil yourself")

Example - 2

https://www.udemy.com/course/design-patterns-python/learn/lecture/13661562

class Creature:
    def __init__(self, name, attack, defense):
        self.defense = defense
        self.attack = attack
        self.name = name

    def __str__(self):
        return f'{self.name} ({self.attack}/{self.defense})'

class CreatureModifier:
    def __init__(self, creature):
        self.creature = creature
        self.next_modifier = None

    def add_modifier(self, modifier):
        if self.next_modifier:
            self.next_modifier.add_modifier(modifier)
        else:
            self.next_modifier = modifier

    def handle(self):
        if self.next_modifier:
            self.next_modifier.handle()

class NoBonusesModifier(CreatureModifier):
    def handle(self):
        print('No bonuses for you!')

class DoubleAttackModifier(CreatureModifier):
    def handle(self):
        print(f'Doubling {self.creature.name}''s attack')
        self.creature.attack *= 2
        super().handle()

class IncreaseDefenseModifier(CreatureModifier):
    def handle(self):
        if self.creature.attack <= 2:
            print(f'Increasing {self.creature.name}''s defense')
            self.creature.defense += 1
        super().handle()

if __name__ == '__main__':
    goblin = Creature('Goblin', 1, 1)
    print(goblin)

    root = CreatureModifier(goblin)

    root.add_modifier(NoBonusesModifier(goblin))

    root.add_modifier(DoubleAttackModifier(goblin))
    root.add_modifier(DoubleAttackModifier(goblin))

    # no effect
    root.add_modifier(IncreaseDefenseModifier(goblin))

    root.handle()  # apply modifiers
    print(goblin)

8.2 Command Design Pattern

A Command Pattern says that encapsulate a request under an object as a command and pass it to invoker object. Invoker object looks for the appropriate object which can handle this command and pass the command to the corresponding object and that object executes the command.

Consider we have a universal remote with four buttons On, Off, Up, and Down. This remote can be configured for any device such as a light, speaker, air conditioner, and the buttons can be configured for the respective device’s use-cases.

  • Example
    • The Up/Down button can be configured to increase/decrease the brightness if the device is a light.
    • The Up/Down button can be configured to increase/decrease the volume if the device is a speaker.
  • https://refactoring.guru/design-patterns/command
class Device:
    def on(self):
        pass
    # def off(self):
    #     pass

class Light(Device):
    def on(self):
        print("Turn on light")

class Speaker(Device):
    def on(self):
        print("Turn on speaker")

class Command:
    def execute(self):
        pass

class OnCommand(Command):
    def __init__(self, device):
        self.device = device

    def execute(self):
        self.device.on()

if __name__ == "__main__":
    device1 = Light()
    device2 = Speaker()
    
    on1 = OnCommand(device1)
    on1.execute()
    on2 = OnCommand(device2)
    on2.execute()

8.3 Iterator Design Pattern

Iterator is a behavioral design pattern that lets you traverse elements of a collection without exposing its underlying representation (list, stack, tree, etc.).

  • The Iterator will commonly contain two methods that perform the following concepts.
    • next: returns the next object in the aggregate (collection, object).
    • has_next: returns a Boolean indicating if the Iterable is at the end of the iteration or not.
  • The iterator protocol requires
    • __iter__() to exposes the iterator
    • __next__() to return each of the iterated elements or stop-iteration

Applicability

https://refactoring.guru/design-patterns/iterator

Use the Iterator pattern when your collection has a complex data structure under the hood, but you want to hide its complexity from clients (either for convenience or security reasons).

Use the Iterator when you want your code to be able to traverse different data structures or when types of these structures are unknown beforehand.

Example - 1

https://sbcode.net/python/iterator/

class Iterable:
    def __init__(self, collection):
        self.index = 0
        self.collection = collection
    
    def has_next(self):
        return self.index < len(self.collection)
    
    def next(self):
        if self.has_next():
            collection = self.collection[self.index]
            self.index += 1
            return collection
        raise Exception("AtEndOfIteratorException", "At End of Iterator")


if __name__ == '__main__':
    AGGREGATES = [1, 22, 33, 'abc']
    # AGGREGATES is a python list that is already iterable by default.
    # but we can create own  iterator on top anyway.
    ITERABLE = Iterable(AGGREGATES)

    while ITERABLE.has_next():
        print(ITERABLE.next())
    ITERABLE.next()

Example - 2

NAMES = ['SEAN','COSMO','EMMY']
ITERATOR = iter(NAMES)
print(ITERATOR.__next__())
print(ITERATOR.__next__())
print(ITERATOR.__next__())


NAMES = ['SEAN','COSMO','EMMY']
ITERATOR = NAMES.__iter__()
print(ITERATOR.__next__())
print(ITERATOR.__next__())
print(ITERATOR.__next__())

Example - 3

https://www.scaler.com/topics/design-patterns/iterator-design-pattern/

class Product:
    def __init__(self, product_name, price):
        self.product_name = product_name
        self.price = price

    def __str__(self):
        return "{}: $ {}".format(self.product_name, self.price)


class CartIterator:
    def __init__(self, products):
        self.position = 0
        self.products = products

    def has_next(self):
        return False if self.position >= len(self.products) else True

    def next(self):
        product = self.products[self.position]
        self.position += 1
        return product

    def remove(self):
        return self.products.pop()


class Cart:
    def __init__(self):
        self.products = []

    def add(self, product):
        self.products.append(product)

    def iterator(self):
        return CartIterator(self.products)


if __name__ == '__main__':
    cart = Cart()
    cart.add(Product("tooth paste", 15))
    cart.add(Product("pen", 30))
    cart.add(Product("bottle", 20))

    print("Displaying Cart:")
    iterator = cart.iterator()

    while iterator.has_next():
        product = iterator.next()
        print(product)

    print("Removing last product returned")
    iterator.remove()

    print("Displaying Cart:")
    iterator = cart.iterator()
    while iterator.has_next():
        product = iterator.next()
        print(product)

Example - 4

https://www.udemy.com/course/design-patterns-python/learn/lecture/13717864

# This has an issue if want to add another ability
class CreatureOld:
    def __init__(self):
        self.strength = 10
        self.agility = 10
        self.intelligence = 10

    @property
    def sum_of_stats(self):
        return self.strength + self.agility + self.intelligence

    @property
    def max_stats(self):
        return max(self.strength, self.agility, self.intelligence)

    @property
    def average_stats(self):
        return self.sum_of_stats / 3.0


# Using Array backed or List backed property
class Creature:
    _strength = 0
    _agility = 1
    _intelligence = 2

    def __int__(self):
        self.stats = [10, 10, 10]

    @property
    def strength(self):
        return self.stats[Creature._strength]

    @strength.setter
    def strength(self, value):
        self.stats[Creature._strength] = value

    @property
    def agility(self):
        return self.stats[Creature._agility]

    @agility.setter
    def agility(self, value):
        self.stats[Creature._agility] = value

    @property
    def intelligence(self):
        return self.stats[Creature._intelligence]

    @intelligence.setter
    def intelligence(self, value):
        self.stats[Creature._intelligence] = value

    @property
    def sum_of_stats(self):
        return sum(self.stats)

    @property
    def max_stats(self):
        return max(self.stats)

    @property
    def average_stats(self):
        return float(sum(self.stats)/len(self.stats))

8.4 Mediator Design Pattern

Objects communicate through the Mediator rather than directly with each other.

Mediator is a behavioral design pattern that lets you reduce chaotic dependencies between objects. The pattern restricts direct communications between the objects and forces them to collaborate only via a mediator object.

Example - 1

https://www.udemy.com/course/design-patterns-python/learn/lecture/13661722

class Person:
    def __init__(self, name):
        self.name = name
        self.rooms = None  # []
    
    def say(self, message):
        self.rooms.broadcast(self.name, message)

class ChatRoom:
    def __init__(self, name):
        self.name = name
        self.people = []
    
    def broadcast(self, source, message):
        if self.people:
            print('Room: ', self.name)
        for p in self.people:
            if p.name != source:
                s = f'{source}: {message}'
                print(f'[{p.name}\'s inbox] {s}')
    
    def join(self, person):
        join_msg = f'{person.name} joins the chat'
        self.broadcast('room', join_msg)
        person.rooms = self # person.rooms.append(self)
        self.people.append(person)


if __name__ == '__main__':
    room1 = ChatRoom('R1')

    john = Person('John')
    alex = Person('Alex')
    simon = Person('Simon')

    room1.join(john)
    room1.join(alex)

    john.say('hi room')
    alex.say('oh, hey john')

    room1.join(simon)
    simon.say('hi everyone!')

8.5 Observer Design Pattern

Observer is a behavioral design pattern that lets you define a subscription mechanism to notify multiple objects about any events that happen to the object they’re observing.

This design pattern is also referred to as Dependents.

class User:
    def __init__(self, userid):
        self.userid = userid


class Group:
    observers = []

    def subscribe(self, user):
        self.observers.append(user)

    def unsubscribe(self, user):
        self.observers.remove(user)

    def notify(self, message):
        for user in self.observers:
            print(f'User {user.userid}', end=', ')
        print(f'- received {message}')

if __name__ == '__main__':
    g1 = Group()

    u1 = User(1)
    u2 = User(2)
    u3 = User(3)

    g1.subscribe(u1)
    g1.subscribe(u2)
    g1.subscribe(u3)

    g1.notify('Message 1')
    g1.unsubscribe(u1)
    g1.notify('Message 2')

8.6 State Design Pattern

State is a behavioral design pattern that lets an object alter its behavior when its internal state changes.

It makes the object flexible to alter its state without handling a lot of if / else conditions.

State pattern ensures loose coupling between the performance of existing states versus the addition of new states.

Example - 1

https://www.udemy.com/course/design-patterns-python/learn/lecture/13682526

# interesting but not practical
class Switch:
    def __init__(self):
        self.state = OffState() # state changes to ON and OFF

    def on(self):
        self.state.on(self)

    def off(self):
        self.state.off(self)

class State:
    def on(self, switch):
        print('Light is already on')

    def off(self, switch):
        print('Light is already off')

class OnState(State):
    def __init__(self):
        print('Light turned on')

    def off(self, switch):
        print('Turning light off...')
        switch.state = OffState()

class OffState(State):
    def __init__(self):
        print('Light turned off')

    def on(self, switch):
        print('Turning light on...')
        switch.state = OnState()

if __name__ == '__main__':
    sw = Switch()

    sw.on()  # Turning light on...
    sw.off()  # Turning light off...
    sw.off()  # Light is already off

Example - 2

https://www.udemy.com/course/design-patterns-python/learn/lecture/13682528

from enum import Enum, auto

class State(Enum):
    OFF_HOOK = auto()
    CONNECTING = auto()
    CONNECTED = auto()
    ON_HOLD = auto()
    ON_HOOK = auto()

class Trigger(Enum):
    CALL_DIALED = auto()
    HUNG_UP = auto()
    CALL_CONNECTED = auto()
    PLACED_ON_HOLD = auto()
    TAKEN_OFF_HOLD = auto()
    LEFT_MESSAGE = auto()

if __name__ == '__main__':
    rules = {
        State.OFF_HOOK: [
            (Trigger.CALL_DIALED, State.CONNECTING)
        ],
        State.CONNECTING: [
            (Trigger.HUNG_UP, State.ON_HOOK),
            (Trigger.CALL_CONNECTED, State.CONNECTED)
        ],
        State.CONNECTED: [
            (Trigger.LEFT_MESSAGE, State.ON_HOOK),
            (Trigger.HUNG_UP, State.ON_HOOK),
            (Trigger.PLACED_ON_HOLD, State.ON_HOLD)
        ],
        State.ON_HOLD: [
            (Trigger.TAKEN_OFF_HOLD, State.CONNECTED),
            (Trigger.HUNG_UP, State.ON_HOOK)
        ]
    }

    state = State.OFF_HOOK
    exit_state = State.ON_HOOK

    while state != exit_state:
        print(f'The phone is currently {state}')

        for i in range(len(rules[state])):
            t = rules[state][i][0]
            print(f'{i}: {t}')

        idx = int(input('Select a trigger:'))
        s = rules[state][idx][1]
        state = s

    print('We are done using the phone.')

8.7 Strategy Design Pattern

Strategy Design Pattern is a behavioral design pattern. It works by abstracting out that part of a class code that is prone to changes into a strategy, which can dynamically be injected at runtime

The Strategy Pattern is similar to the State Pattern, except that the client passes in the algorithm that the context should run.

Example - 1

https://www.udemy.com/course/design-patterns-python/learn/lecture/13676780

from abc import ABC
from enum import Enum, auto

class OutputFormat(Enum):
    MARKDOWN = auto()
    HTML = auto()

# not required but a good idea
class ListStrategy(ABC):
    def start(self, buffer): pass
    def end(self, buffer): pass
    def add_list_item(self, buffer, item): pass

class MarkdownListStrategy(ListStrategy):
    def add_list_item(self, buffer, item):
        buffer.append(f' * {item}\n')

class HtmlListStrategy(ListStrategy):
    def start(self, buffer):
        buffer.append('<ul>\n')

    def end(self, buffer):
        buffer.append('</ul>\n')

    def add_list_item(self, buffer, item):
        buffer.append(f'  <li>{item}</li>\n')

class TextProcessor:
    def __init__(self, ):
        self.buffer = []
        self.list_strategy = None

    def append_list(self, items):
        self.list_strategy.start(self.buffer)
        for item in items:
            self.list_strategy.add_list_item(self.buffer, item)
        self.list_strategy.end(self.buffer)

    def set_output_format(self, output_format):
        if output_format == OutputFormat.MARKDOWN:
            self.list_strategy = MarkdownListStrategy()
        elif output_format == OutputFormat.HTML:
            self.list_strategy = HtmlListStrategy()

    def clear(self):
        self.buffer.clear()

    def __str__(self):
        return ''.join(self.buffer)

if __name__ == '__main__':
    item_list = ['foo', 'bar', 'baz']

    tp = TextProcessor()
    tp.set_output_format(OutputFormat.MARKDOWN)
    tp.append_list(item_list)
    print(tp)
    
    tp.clear()
    tp.set_output_format(OutputFormat.HTML)
    tp.append_list(item_list)
    print(tp)

8.8 Template Method Design Pattern

Template Method is a behavioral design pattern that allow us to defines the skeleton of an algorithm in the superclass but lets subclasses override specific steps of the algorithm without changing its structure.

  • Template Method Patterns encapsulate algorithms using the concept of inheritance.
  • Strategy Patterns encapsulate algorithms using the concept of composition.

Example - 1

The Abstract Class defines a template method that contains a skeleton of some algorithm, composed of calls to (usually) abstract primitive operations.

Concrete subclasses should implement these operations, but leave the template method itself intact.

https://refactoring.guru/design-patterns/template-method/python/example

from abc import ABC, abstractmethod

class AbstractClass(ABC):
    def template_method(self) -> None:
        """The template method defines the skeleton of an algorithm."""
        self.base_operation1()
        self.required_operations1()
        self.base_operation2()

    def base_operation1(self) -> None:
        print("AbstractClass says: I am doing the bulk of the work")
    def base_operation2(self) -> None:
        print("AbstractClass says: But I let subclasses override some operations")

    # These operations have to be implemented in subclasses.
    @abstractmethod
    def required_operations1(self) -> None: pass

class ConcreteClass1(AbstractClass):
    def required_operations1(self) -> None:
        print("ConcreteClass1 says: Implemented Operation1")

class ConcreteClass2(AbstractClass):
    def required_operations1(self) -> None:
        print("ConcreteClass2 says: Implemented Operation1")

def client_code(abstract_class: AbstractClass) -> None:
    abstract_class.template_method()


if __name__ == "__main__":
    client_code(ConcreteClass1())
    print("")
    client_code(ConcreteClass2())

Example - 2

https://www.udemy.com/course/design-patterns-python/learn/lecture/13676798

from abc import ABC

class Game(ABC):
    def __init__(self, number_of_players):
        self.number_of_players = number_of_players
        self.current_player = 0

    def run(self):
        self.start()
        while not self.have_winner:
            self.take_turn()
        print(f'Player {self.winning_player} wins!')

    def start(self): pass

    @property
    def have_winner(self): pass

    def take_turn(self): pass

    @property
    def winning_player(self): pass

class Chess(Game):
    def __init__(self):
        super().__init__(2)
        self.max_turns = 10
        self.turn = 1

    def start(self):
        print(f'Starting a game of chess with {self.number_of_players} players.')

    @property
    def have_winner(self):
        return self.turn == self.max_turns

    def take_turn(self):
        print(f'Turn {self.turn} taken by player {self.current_player}')
        self.turn += 1
        self.current_player = 1 - self.current_player

    @property
    def winning_player(self):
        return self.current_player

if __name__ == '__main__':
    chess = Chess()
    chess.run()

8.9 Visitor Design Pattern

Visitor Design Pattern allows adding extra behaviours to entire hierarchies of classes without modifying every class in the hierarchy.

Visitor isn’t a very common pattern because of its complexity and narrow applicability

Applicability

Use the Visitor when you need to perform an operation on all elements of a complex object structure (for example, an object tree).

Example - 1

https://sbcode.net/python/visitor/

class Element:
    def __init__(self, name, value, parent=None):
        self.name = name
        self.value = value
        self.elements = set()
        if parent:
            parent.elements.add(self)

    def accept(self, visitor):
        """required by the Visitor that will traverse"""
        for element in self.elements:
            element.accept(visitor)
        visitor.visit(self)

# The Client Creating an example object hierarchy.
Element_A = Element("A", 1)
Element_B = Element("B", 2, Element_A)
Element_C = Element("C", 3, Element_A)
Element_D = Element("D", 4, Element_B)



# Now Rather than changing the Element class to support custom operations, we can utilise 
# the accept method that was
# implemented in the Element class because of the addition of the IVisitable interface
class PrintElementNamesVisitor:
    """Create a visitor that prints the Element names"""
    @staticmethod
    def visit(element):
        print(element.name)

# Using the PrintElementNamesVisitor to traverse the object hierarchy
Element_A.accept(PrintElementNamesVisitor)


class CalculateElementTotalsVisitor:
    """Create a visitor that totals the Element values"""
    total_value = 0

    @classmethod
    def visit(cls, element):
        cls.total_value += element.value
        return cls.total_value

# Using the CalculateElementTotalsVisitor to traverse the
# object hierarchy
TOTAL = CalculateElementTotalsVisitor()
Element_A.accept(CalculateElementTotalsVisitor)
print(TOTAL.total_value)