📝 Topics Covered
- 1. What are Behavioral Patterns?
- 2. Chain of Responsibility Design Pattern
- 3. Command Design Pattern
- 4. Iterator Design Pattern
- 5. Mediator Design Pattern
- 6. Observer Design Pattern
- 7. State Design Pattern
- 8. Strategy Design Pattern
- 9. Template Method Design Pattern
- 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:
- Chain of Responsibility: Passes requests along a chain of handlers.
- Command: Encapsulates a request as an object, enabling parameterization and undo operations.
- Iterator: Traverses elements of a collection without exposing its underlying structure.
- Mediator: Restricts direct communications between objects, forcing them to collaborate via a mediator.
- Observer: Establishes a one-to-many subscription mechanism to notify dependents of state changes.
- State: Allows an object to alter its behavior when its internal state changes.
- Strategy: Defines a family of algorithms, encapsulating each one, and making them interchangeable at runtime.
- Template Method: Defines the skeleton of an algorithm in a superclass, deferring steps to subclasses.
- 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, raisingStopIterationwhen 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}")