📝 Topics Covered

  1. 1. What are Behavioral Patterns?
  2. 2. Chain of Responsibility Design Pattern
  3. 3. Command Design Pattern
  4. 4. Iterator Design Pattern
  5. 5. Mediator Design Pattern
  6. 6. Observer Design Pattern
  7. 7. State Design Pattern
  8. 8. Strategy Design Pattern
  9. 9. Template Method Design Pattern
  10. 10. Visitor Design Pattern

1. What are Behavioral Patterns?

Behavioral design patterns are concerned with algorithms and the assignment of responsibilities between objects. They describe not just patterns of objects or classes but also the patterns of communication between them. These patterns characterize complex control flows that are difficult to follow at runtime.

In this guide, we will cover the following nine core Behavioral patterns:

  1. Chain of Responsibility: Passes requests along a chain of handlers.
  2. Command: Encapsulates a request as an object, enabling parameterization and undo operations.
  3. Iterator: Traverses elements of a collection without exposing its underlying structure.
  4. Mediator: Restricts direct communications between objects, forcing them to collaborate via a mediator.
  5. Observer: Establishes a one-to-many subscription mechanism to notify dependents of state changes.
  6. State: Allows an object to alter its behavior when its internal state changes.
  7. Strategy: Defines a family of algorithms, encapsulating each one, and making them interchangeable at runtime.
  8. Template Method: Defines the skeleton of an algorithm in a superclass, deferring steps to subclasses.
  9. Visitor: Separates an algorithm from the object structure on which it operates, allowing behaviors to be added dynamically.

2. Chain of Responsibility Design Pattern

Chain of Responsibility 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.

  • Typical Real-world Use Cases:
    • Vending machine bill/coin validators.
    • ATM cash dispensing mechanisms.
    • Hierarchical application logging services (Info -> Warn -> Error).

2.1 CoR Example 1: ATM Cash Dispenser

import sys
from abc import ABC, abstractmethod

class IDispenser(ABC):
    @abstractmethod
    def next_successor(self, successor):
        pass

    @abstractmethod
    def handle(self, amount):
        pass


class Dispenser(IDispenser):
    def __init__(self):
        self._successor = None
    
    def dispense_amount(self, amount, note_value):
        if amount >= note_value:
            num = amount // note_value
            remainder = amount % note_value
            print(f"Dispensing {num} x Rs.{note_value} notes via {self.__class__.__name__}")
            if remainder != 0 and self._successor:
                self._successor.handle(remainder)
        elif self._successor:
            self._successor.handle(amount)


class Dispenser50(Dispenser):
    def next_successor(self, successor):
        self._successor = successor

    def handle(self, amount):
        self.dispense_amount(amount, 50)


class Dispenser20(Dispenser):
    def next_successor(self, successor):
        self._successor = successor

    def handle(self, amount):
        self.dispense_amount(amount, 20)


class Dispenser10(Dispenser):
    def next_successor(self, successor):
        self._successor = successor

    def handle(self, amount):
        self.dispense_amount(amount, 10)


class ATMDispenserChain:
    def __init__(self):
        self.chain1 = Dispenser50()
        self.chain2 = Dispenser20()
        self.chain3 = Dispenser10()
        
        # Link the chain successors
        self.chain1.next_successor(self.chain2)
        self.chain2.next_successor(self.chain3)


# Usage
if __name__ == '__main__':
    amount_input = 130
    if amount_input < 10 or amount_input % 10 != 0:
        print("Error: Amount must be a positive multiple of 10.")
        sys.exit()
        
    atm = ATMDispenserChain()
    print(f"--- Dispensing Rs.{amount_input} ---")
    atm.chain1.handle(amount_input)

2.2 CoR Example 2: Creature Attack Modifier Chain

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

    def __str__(self):
        return f'{self.name} (Atk: {self.attack} / Def: {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("Curse! No modifiers allowed for this creature.")
        # Stops the propagation chain entirely


class DoubleAttackModifier(CreatureModifier):
    def handle(self):
        print(f"Doubling {self.creature.name}'s attack power.")
        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()


# Usage
if __name__ == '__main__':
    goblin = Creature('Goblin', 1, 1)
    print(f"Initial: {goblin}")

    root = CreatureModifier(goblin)
    
    # Enable double attack modifiers
    root.add_modifier(DoubleAttackModifier(goblin))
    root.add_modifier(DoubleAttackModifier(goblin))
    
    # Increments defense if attack is small
    root.add_modifier(IncreaseDefenseModifier(goblin))

    print("--- Applying Modifiers ---")
    root.handle()
    print(f"Final Stats: {goblin}")

3. Command Design Pattern

The Command pattern encapsulates a request as a standalone object containing all information about the request. This transformation lets you pass requests as method arguments, delay or queue a request’s execution, and support undoable operations.

  • Invoker Concept: The invoker triggers the command. It doesn’t know details about how the command executes; it simply calls execute().
  • Receiver Concept: The concrete receiver object executes the low-level business logic.

🎥 Tutorial: Command Design Pattern

3.1 Command Example: Device Remote Controls

from abc import ABC, abstractmethod

class Device(ABC):
    @abstractmethod
    def on(self):
        pass


class Light(Device):
    def on(self):
        print("Light turned ON.")


class Speaker(Device):
    def on(self):
        print("Speaker turned ON.")


# The Command Abstraction
class Command(ABC):
    @abstractmethod
    def execute(self):
        pass


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

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


# Usage
if __name__ == "__main__":
    living_room_light = Light()
    soundbar = Speaker()
    
    light_command = OnCommand(living_room_light)
    speaker_command = OnCommand(soundbar)
    
    # Triggering execution
    light_command.execute()
    speaker_command.execute()

4. Iterator Design Pattern

Iterator lets you traverse elements of a collection without exposing its underlying complex representation (e.g. lists, stacks, trees, or graphs).

  • Standard Interface Methods:
    • next(): Returns the next object in the collection.
    • has_next(): Returns a boolean indicating if the iteration has reached the end.
  • Python Protocol Requirements:
    • __iter__() exposes the iterator class.
    • __next__() returns elements one by one, raising StopIteration when complete.

4.1 Iterator Example 1: Basic Custom Traverser

class CustomIterable:
    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():
            item = self.collection[self.index]
            self.index += 1
            return item
        raise StopIteration("At the end of iteration.")


# Usage
if __name__ == '__main__':
    data_list = [10, 20, 30, 'Custom Data']
    iterable = CustomIterable(data_list)

    print("Traversing collection:")
    while iterable.has_next():
        print(iterable.next())

4.2 Iterator Example 2: Pythonic Native Protocols

names = ['Amrit', 'Alex', 'Simon']

# Under the hood, this is how iterators operate in Python loops
iterator = iter(names)
print(next(iterator))
print(next(iterator))
print(next(iterator))

4.3 Iterator Example 3: Shopping Cart Traverser

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

    def __str__(self):
        return f"{self.name}: ${self.price}"


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

    def has_next(self):
        return self.position < len(self.products)

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

    def remove_last(self):
        if self.products:
            return self.products.pop()
        return None


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

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

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


# Usage
if __name__ == '__main__':
    cart = Cart()
    cart.add(Product("Toothpaste", 15))
    cart.add(Product("Ballpoint Pen", 30))
    cart.add(Product("Water Bottle", 20))

    print("--- Cart Items ---")
    it = cart.iterator()
    while it.has_next():
        print(it.next())

    print("\nRemoving last added product...")
    it.remove_last()

    print("\n--- Updated Cart Items ---")
    it = cart.iterator()
    while it.has_next():
        print(it.next())

4.4 Iterator Example 4: List-Backed Properties (Memory Optimization)

class Creature:
    # Index definitions
    _strength = 0
    _agility = 1
    _intelligence = 2

    def __init__(self):
        self.stats = [10, 15, 12]  # Internally backed by a list

    @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 sum(self.stats) / len(self.stats)


# Usage
if __name__ == '__main__':
    hero = Creature()
    print(f"Hero Stats -> Total: {hero.sum_of_stats}, Max: {hero.max_stats}, Avg: {hero.average_stats:.2f}")

5. Mediator Design Pattern

The Mediator pattern reduces chaotic direct dependencies between components. It restricts direct communication between objects and forces them to collaborate only via a central mediator object.

5.1 Mediator Example: Chatroom Orchestration

class Person:
    def __init__(self, name):
        self.name = name
        self.chat_room = None
        self.inbox = []
    
    def say(self, message):
        if self.chat_room:
            self.chat_room.broadcast(self.name, message)

    def receive(self, sender, message):
        inbox_line = f"[{sender}]: {message}"
        self.inbox.append(inbox_line)
        print(f"[{self.name}'s Inbox] -> {inbox_line}")


class ChatRoom:
    def __init__(self, name):
        self.name = name
        self.users = []
    
    def broadcast(self, sender, message):
        print(f"\n--- ChatRoom '{self.name}' Broadcast from {sender} ---")
        for user in self.users:
            if user.name != sender:
                user.receive(sender, message)
    
    def join(self, person):
        join_msg = f"{person.name} has joined the room."
        self.broadcast('System', join_msg)
        person.chat_room = self
        self.users.append(person)


# Usage
if __name__ == '__main__':
    lobby = ChatRoom('Lobby')

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

    lobby.join(john)
    lobby.join(alex)

    john.say('Hello everyone!')
    alex.say('Hey John, welcome!')

    lobby.join(simon)
    simon.say('Glad to join the chat!')

6. Observer Design Pattern

Observer establishes a subscription mechanism to notify multiple objects (observers) about any events or state changes happening to the object they are observing (subject/observable).

6.1 Observer Example: Group Notifications

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


class NotificationGroup:
    def __init__(self):
        self.subscribers = []

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

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

    def notify(self, message):
        for user in self.subscribers:
            print(f"User {user.user_id} notified of update -> '{message}'")


# Usage
if __name__ == '__main__':
    alerts = NotificationGroup()

    user1 = User(101)
    user2 = User(102)
    user3 = User(103)

    alerts.subscribe(user1)
    alerts.subscribe(user2)
    alerts.subscribe(user3)

    alerts.notify('Server maintenance at 12:00 PM')
    alerts.unsubscribe(user1)
    
    print("\n--- After Unsubscribing User 101 ---")
    alerts.notify('System updates completed')

7. State Design Pattern

The State pattern lets an object alter its behavior when its internal state changes. The object will appear to change its class, shielding client code from complex conditional branches.

7.1 State Example 1: Classic Switch Mechanics

class Switch:
    def __init__(self):
        self.state = OffState()

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

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


class State:
    def on(self, switch):
        print("System is already ON.")

    def off(self, switch):
        print("System is already OFF.")


class OnState(State):
    def __init__(self):
        print("Switch State: Turned ON.")

    def off(self, switch):
        print("Turning switch OFF...")
        switch.state = OffState()


class OffState(State):
    def __init__(self):
        print("Switch State: Turned OFF.")

    def on(self, switch):
        print("Turning switch ON...")
        switch.state = OnState()


# Usage
if __name__ == '__main__':
    light_switch = Switch()
    
    # Turn switch on
    light_switch.on()
    
    # Turn switch off
    light_switch.off()
    
    # Try to turn off again
    light_switch.off()

7.2 State Example 2: Phone Line Trigger Engine

from enum import Enum, auto

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


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


# Usage
if __name__ == '__main__':
    # State transitions registry map
    rules = {
        PhoneState.OFF_HOOK: [
            (PhoneTrigger.CALL_DIALED, PhoneState.CONNECTING)
        ],
        PhoneState.CONNECTING: [
            (PhoneTrigger.HUNG_UP, PhoneState.ON_HOOK),
            (PhoneTrigger.CALL_CONNECTED, PhoneState.CONNECTED)
        ],
        PhoneState.CONNECTED: [
            (PhoneTrigger.LEFT_MESSAGE, PhoneState.ON_HOOK),
            (PhoneTrigger.HUNG_UP, PhoneState.ON_HOOK),
            (PhoneTrigger.PLACED_ON_HOLD, PhoneState.ON_HOLD)
        ],
        PhoneState.ON_HOLD: [
            (PhoneTrigger.TAKEN_OFF_HOLD, PhoneState.CONNECTED),
            (PhoneTrigger.HUNG_UP, PhoneState.ON_HOOK)
        ]
    }

    current_state = PhoneState.OFF_HOOK
    termination_state = PhoneState.ON_HOOK

    # Simulated lifecycle run
    print(f"Phone current status: {current_state}")
    
    # Trigger Dialed -> Connecting
    dial_trigger = rules[current_state][0]
    print(f"Trigger action: {dial_trigger[0]}")
    current_state = dial_trigger[1]
    
    # Trigger Connected -> Connected
    print(f"Phone current status: {current_state}")
    connect_trigger = rules[current_state][1]
    print(f"Trigger action: {connect_trigger[0]}")
    current_state = connect_trigger[1]
    
    print(f"Final phone state: {current_state}")

8. Strategy Design Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime. Strategy lets the algorithm vary independently from the clients that use it.

  • Strategy vs. State:
    • State Pattern: States are aware of other states and handle transitions between them.
    • Strategy Pattern: Strategies are completely independent of one another; the client is responsible for choosing and injecting the strategy.

8.1 Strategy Example: HTML/Markdown List Strategies

from abc import ABC, abstractmethod
from enum import Enum, auto

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


class ListStrategy(ABC):
    def start(self, buffer): 
        pass
    
    def end(self, buffer): 
        pass
    
    @abstractmethod
    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):
        if self.list_strategy:
            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, format_type):
        if format_type == FormatType.MARKDOWN:
            self.list_strategy = MarkdownListStrategy()
        elif format_type == FormatType.HTML:
            self.list_strategy = HtmlListStrategy()

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

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


# Usage
if __name__ == '__main__':
    items = ['Python', 'Golang', 'Rust']

    processor = TextProcessor()
    
    print("--- Markdown List ---")
    processor.set_output_format(FormatType.MARKDOWN)
    processor.append_list(items)
    print(processor)
    
    processor.clear()
    
    print("--- HTML List ---")
    processor.set_output_format(FormatType.HTML)
    processor.append_list(items)
    print(processor)

9. Template Method Design Pattern

Template Method defines the skeleton of an algorithm in a superclass but lets subclasses override specific steps of the algorithm without changing its overall structure.

  • Inheritance vs. Composition:
    • Template Method Pattern: Encapsulates algorithms using inheritance (subclass overrides primitive methods).
    • Strategy Pattern: Encapsulates algorithms using composition (delegates behavior to an injected helper).

9.1 Template Method Example 1: Generic Processing Skeletons

from abc import ABC, abstractmethod

class DataProcessor(ABC):
    def process(self) -> None:
        """The template method defining execution flow."""
        self.load_data()
        self.parse_data()
        self.save_data()

    def load_data(self) -> None:
        print("DataProcessor: Fetching raw data streams...")

    def save_data(self) -> None:
        print("DataProcessor: Writing processed outputs to database.")

    @abstractmethod
    def parse_data(self) -> None: 
        pass


class XMLDataProcessor(DataProcessor):
    def parse_data(self) -> None:
        print("XMLDataProcessor: Parsing raw XML documents into trees.")


class JSONDataProcessor(DataProcessor):
    def parse_data(self) -> None:
        print("JSONDataProcessor: Loading raw JSON streams into dictionaries.")


# Usage
if __name__ == "__main__":
    print("--- Processing XML ---")
    xml_job = XMLDataProcessor()
    xml_job.process()

    print("\n--- Processing JSON ---")
    json_job = JSONDataProcessor()
    json_job.process()

9.2 Template Method Example 2: Turn-Based Game Engine (Chess)

from abc import ABC, abstractmethod

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

    def run(self):
        """The Template Method orchestrating game loop execution"""
        self.start()
        while not self.have_winner:
            self.take_turn()
        print(f'Player {self.winning_player} wins the game!')

    @abstractmethod
    def start(self): 
        pass

    @property
    @abstractmethod
    def have_winner(self): 
        pass

    @abstractmethod
    def take_turn(self): 
        pass

    @property
    @abstractmethod
    def winning_player(self): 
        pass


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

    def start(self):
        print(f'Chess Match Started: {self.number_of_players} players registered.')

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

    def take_turn(self):
        print(f"Turn #{self.turn} - Action taken by Player {self.current_player}")
        self.turn += 1
        # Alternate player turn
        self.current_player = 1 - self.current_player

    @property
    def winning_player(self):
        # The last active player who took a turn wins
        return self.current_player


# Usage
if __name__ == '__main__':
    chess_match = Chess()
    chess_match.run()

10. Visitor Design Pattern

Visitor allows you to add extra operations or behaviors to entire hierarchies of classes without modifying the existing classes directly.

  • Complexity Caution: Visitor is not a highly common pattern because it requires complex double-dispatch interfaces and is best suited for stable hierarchies (e.g., compilers traversing syntax trees).

10.1 Visitor Example: Expression Tree Traversal

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 double-dispatch accept method"""
        for element in self.elements:
            element.accept(visitor)
        visitor.visit(self)


# Define a visitor to print element names
class PrintNamesVisitor:
    @staticmethod
    def visit(element):
        print(f"Visited Node Name: {element.name}")


# Define a visitor to calculate element values
class CalculateTotalsVisitor:
    def __init__(self):
        self.total = 0

    def visit(self, element):
        self.total += element.value


# Usage
if __name__ == '__main__':
    # Build tree hierarchy
    root_node = Element("Root", 10)
    child_a = Element("Child_A", 20, root_node)
    child_b = Element("Child_B", 30, root_node)
    sub_child_a = Element("Sub_Child_A", 5, child_a)

    print("--- Printing Element Names ---")
    root_node.accept(PrintNamesVisitor)

    print("\n--- Calculating Totals ---")
    totals_visitor = CalculateTotalsVisitor()
    root_node.accept(totals_visitor)
    print(f"Total summed value across expressions: {totals_visitor.total}")