Table of Contents
- What Are Design Patterns?
- Structural Design Patterns
- Behavioral Design Patterns
- Structural vs. Behavioral: Key Differences
- When to Use Which?
- Conclusion
- References
What Are Design Patterns?
Design patterns are reusable solutions to recurring problems in software design. They are not code snippets but templates that guide how to structure classes and objects to solve specific challenges. The “Gang of Four” (GoF) formalized 23 core patterns in their 1994 book Design Patterns: Elements of Reusable Object-Oriented Software, categorizing them into three groups:
- Creational: Focus on object instantiation (e.g., Singleton, Factory).
- Structural: Focus on object composition (e.g., Adapter, Decorator).
- Behavioral: Focus on object interaction (e.g., Observer, Strategy).
This blog focuses on Structural and Behavioral patterns, exploring their use cases, implementations, and differences.
Structural Design Patterns
Structural patterns focus on how objects and classes are composed to form larger structures. They simplify relationships between entities, enabling flexible and efficient system architectures. Common goals include adapting interfaces, combining objects, or hiding complexity.
Adapter Pattern
Purpose: Convert the interface of a class into another interface that clients expect. Useful for integrating legacy code or third-party libraries with incompatible interfaces.
Use Case: Suppose you have a legacy payment processor with a method make_payment(amount), but your new e-commerce system expects a process_payment(amount) interface.
Python Example:
# Legacy payment processor (incompatible interface)
class LegacyPaymentProcessor:
def make_payment(self, amount: float) -> None:
print(f"Legacy: Processing payment of ${amount}")
# Target interface expected by the new system
class PaymentProcessor:
def process_payment(self, amount: float) -> None:
raise NotImplementedError
# Adapter: Wraps LegacyPaymentProcessor to match PaymentProcessor
class PaymentAdapter(PaymentProcessor):
def __init__(self, legacy_processor: LegacyPaymentProcessor):
self.legacy_processor = legacy_processor
def process_payment(self, amount: float) -> None:
# Adapt the new interface to the legacy method
self.legacy_processor.make_payment(amount)
# Client code (new system)
def main():
legacy_processor = LegacyPaymentProcessor()
adapter = PaymentAdapter(legacy_processor)
adapter.process_payment(100.0) # Works with the new interface!
if __name__ == "__main__":
main()
Output:
Legacy: Processing payment of $100.0
Decorator Pattern
Purpose: Dynamically add responsibilities to an object without altering its structure. Ideal for adding features (e.g., logging, caching) to objects at runtime.
Use Case: A coffee shop where customers can add toppings (milk, sugar, foam) to their coffee, with each topping increasing the cost.
Python Example:
# Base component: Coffee
class Coffee:
def cost(self) -> float:
return 5.0 # Base coffee cost
def description(self) -> str:
return "Basic Coffee"
# Decorator base class (follows the Coffee interface)
class CoffeeDecorator(Coffee):
def __init__(self, coffee: Coffee):
self._coffee = coffee
def cost(self) -> float:
return self._coffee.cost()
def description(self) -> str:
return self._coffee.description()
# Concrete decorators
class MilkDecorator(CoffeeDecorator):
def cost(self) -> float:
return super().cost() + 1.5 # Add milk cost
def description(self) -> str:
return super().description() + ", Milk"
class SugarDecorator(CoffeeDecorator):
def cost(self) -> float:
return super().cost() + 0.5 # Add sugar cost
def description(self) -> str:
return super().description() + ", Sugar"
# Client code
def main():
coffee = Coffee()
coffee_with_milk = MilkDecorator(coffee)
coffee_with_milk_sugar = SugarDecorator(coffee_with_milk)
print(f"Order: {coffee_with_milk_sugar.description()}")
print(f"Total Cost: ${coffee_with_milk_sugar.cost()}")
if __name__ == "__main__":
main()
Output:
Order: Basic Coffee, Milk, Sugar
Total Cost: $7.0
Composite Pattern
Purpose: Treat individual objects and compositions of objects uniformly. Useful for tree-like structures (e.g., file systems, organization hierarchies).
Use Case: A file system where files and directories are treated as “components”—both have a get_size() method, but directories aggregate sizes of their children.
Python Example:
from abc import ABC, abstractmethod
# Component interface
class FileSystemComponent(ABC):
@abstractmethod
def get_size(self) -> int:
pass
# Leaf: Individual file
class File(FileSystemComponent):
def __init__(self, name: str, size: int):
self.name = name
self.size = size
def get_size(self) -> int:
return self.size
# Composite: Directory (contains files/directories)
class Directory(FileSystemComponent):
def __init__(self, name: str):
self.name = name
self.children: list[FileSystemComponent] = []
def add_child(self, component: FileSystemComponent) -> None:
self.children.append(component)
def remove_child(self, component: FileSystemComponent) -> None:
self.children.remove(component)
def get_size(self) -> int:
# Sum sizes of all children
return sum(child.get_size() for child in self.children)
# Client code
def main():
# Create files
file1 = File("document.txt", 1024)
file2 = File("image.jpg", 2048)
# Create directories
docs_dir = Directory("Documents")
docs_dir.add_child(file1)
root_dir = Directory("Root")
root_dir.add_child(docs_dir)
root_dir.add_child(file2)
print(f"Total size of Root directory: {root_dir.get_size()} bytes")
if __name__ == "__main__":
main()
Output:
Total size of Root directory: 3072 bytes
Proxy Pattern
Purpose: Provide a placeholder for another object to control access to it (e.g., lazy initialization, access control, or logging).
Use Case: A database connection that is expensive to create. Use a proxy to delay initialization until the connection is first needed.
Python Example:
class RealDatabase:
def __init__(self):
# Simulate expensive connection setup
print("Connecting to database... (takes 2s)")
import time
time.sleep(2)
print("Connected!")
def query(self, sql: str) -> None:
print(f"Executing query: {sql}")
class DatabaseProxy:
def __init__(self):
self._real_db = None # Lazy initialization
def query(self, sql: str) -> None:
if not self._real_db:
self._real_db = RealDatabase() # Initialize only when needed
self._real_db.query(sql)
# Client code
def main():
db = DatabaseProxy()
print("Proxy created (no connection yet)")
db.query("SELECT * FROM users") # Triggers connection
db.query("INSERT INTO logs ...") # Uses existing connection
if __name__ == "__main__":
main()
Output:
Proxy created (no connection yet)
Connecting to database... (takes 2s)
Connected!
Executing query: SELECT * FROM users
Executing query: INSERT INTO logs ...
Facade Pattern
Purpose: Provide a simplified interface to a complex subsystem. Reduces dependencies between clients and subsystem components.
Use Case: A home theater system with multiple components (DVD player, projector, speakers). Instead of controlling each component individually, use a facade to coordinate them.
Python Example:
# Complex subsystem components
class DVDPlayer:
def play(self, movie: str) -> None:
print(f"DVD: Playing '{movie}'")
class Projector:
def turn_on(self) -> None:
print("Projector: On")
def set_input(self, source: str) -> None:
print(f"Projector: Input set to {source}")
class Speakers:
def turn_on(self) -> None:
print("Speakers: On")
def set_volume(self, level: int) -> None:
print(f"Speakers: Volume {level}")
# Facade
class HomeTheaterFacade:
def __init__(self):
self.dvd = DVDPlayer()
self.projector = Projector()
self.speakers = Speakers()
def watch_movie(self, movie: str) -> None:
print("\n=== Preparing to watch movie ===")
self.projector.turn_on()
self.projector.set_input("DVD")
self.speakers.turn_on()
self.speakers.set_volume(15)
self.dvd.play(movie)
# Client code
def main():
theater = HomeTheaterFacade()
theater.watch_movie("Inception")
if __name__ == "__main__":
main()
Output:
=== Preparing to watch movie ===
Projector: On
Projector: Input set to DVD
Speakers: On
Speakers: Volume 15
DVD: Playing 'Inception'
Behavioral Design Patterns
Behavioral patterns focus on how objects interact and distribute responsibility. They define algorithms, workflows, and communication protocols to ensure flexible and maintainable interaction between components.
Observer Pattern
Purpose: Define a one-to-many dependency between objects. When one object (subject) changes state, all its dependents (observers) are notified and updated automatically.
Use Case: A weather station where multiple displays (e.g., temperature, humidity) update whenever new weather data is available.
Python Example:
from abc import ABC, abstractmethod
# Subject interface
class Subject(ABC):
@abstractmethod
def attach(self, observer: "Observer") -> None:
pass
@abstractmethod
def detach(self, observer: "Observer") -> None:
pass
@abstractmethod
def notify(self) -> None:
pass
# Observer interface
class Observer(ABC):
@abstractmethod
def update(self, temperature: float, humidity: float) -> None:
pass
# Concrete Subject: Weather Station
class WeatherStation(Subject):
def __init__(self):
self._observers: list[Observer] = []
self._temperature: float = 0.0
self._humidity: float = 0.0
def attach(self, observer: Observer) -> None:
self._observers.append(observer)
def detach(self, observer: Observer) -> None:
self._observers.remove(observer)
def notify(self) -> None:
for observer in self._observers:
observer.update(self._temperature, self._humidity)
def set_measurements(self, temperature: float, humidity: float) -> None:
self._temperature = temperature
self._humidity = humidity
self.notify() # Notify observers of new data
# Concrete Observers: Displays
class TemperatureDisplay(Observer):
def update(self, temperature: float, humidity: float) -> None:
print(f"Temperature Display: {temperature}°C")
class HumidityDisplay(Observer):
def update(self, temperature: float, humidity: float) -> None:
print(f"Humidity Display: {humidity}%")
# Client code
def main():
station = WeatherStation()
temp_display = TemperatureDisplay()
humidity_display = HumidityDisplay()
station.attach(temp_display)
station.attach(humidity_display)
station.set_measurements(22.5, 65.0) # Triggers updates
station.set_measurements(24.0, 60.0)
if __name__ == "__main__":
main()
Output:
Temperature Display: 22.5°C
Humidity Display: 65.0%
Temperature Display: 24.0°C
Humidity Display: 60.0%
Strategy Pattern
Purpose: Encapsulate a family of algorithms and make them interchangeable. Allows selecting an algorithm at runtime without changing client code.
Use Case: A payment system supporting multiple payment methods (credit card, PayPal, Bitcoin). Each method is a “strategy” that can be swapped dynamically.
Python Example:
from abc import ABC, abstractmethod
# Strategy interface
class PaymentStrategy(ABC):
@abstractmethod
def pay(self, amount: float) -> None:
pass
# Concrete strategies
class CreditCardStrategy(PaymentStrategy):
def __init__(self, card_number: str):
self.card_number = card_number
def pay(self, amount: float) -> None:
print(f"Paid ${amount} via Credit Card (****-****-****-{self.card_number[-4:]})")
class PayPalStrategy(PaymentStrategy):
def __init__(self, email: str):
self.email = email
def pay(self, amount: float) -> None:
print(f"Paid ${amount} via PayPal ({self.email})")
# Context: Uses a strategy
class ShoppingCart:
def __init__(self, payment_strategy: PaymentStrategy):
self._payment_strategy = payment_strategy
def checkout(self, amount: float) -> None:
self._payment_strategy.pay(amount)
# Client code
def main():
# Pay with credit card
cart = ShoppingCart(CreditCardStrategy("4111-1111-1111-1234"))
cart.checkout(99.99)
# Switch to PayPal
cart = ShoppingCart(PayPalStrategy("[email protected]"))
cart.checkout(49.99)
if __name__ == "__main__":
main()
Output:
Paid $99.99 via Credit Card (****-****-****-1234)
Paid $49.99 via PayPal ([email protected])
Command Pattern
Purpose: Encapsulate a request as an object, allowing parameterization of clients with different requests, queuing of requests, or support for undo/redo.
Use Case: A remote control with buttons that trigger actions (e.g., turn on TV, adjust volume). Each button maps to a “command” object.
Python Example:
from abc import ABC, abstractmethod
# Command interface
class Command(ABC):
@abstractmethod
def execute(self) -> None:
pass
@abstractmethod
def undo(self) -> None:
pass
# Receiver: Object that performs the action
class TV:
def turn_on(self) -> None:
print("TV: On")
def turn_off(self) -> None:
print("TV: Off")
def volume_up(self) -> None:
print("TV: Volume +")
def volume_down(self) -> None:
print("TV: Volume -")
# Concrete commands
class TurnOnTVCommand(Command):
def __init__(self, tv: TV):
self.tv = tv
def execute(self) -> None:
self.tv.turn_on()
def undo(self) -> None:
self.tv.turn_off()
class VolumeUpCommand(Command):
def __init__(self, tv: TV):
self.tv = tv
def execute(self) -> None:
self.tv.volume_up()
def undo(self) -> None:
self.tv.volume_down()
# Invoker: Sends commands
class RemoteControl:
def __init__(self):
self._command: Command | None = None
def set_command(self, command: Command) -> None:
self._command = command
def press_button(self) -> None:
if self._command:
self._command.execute()
def press_undo(self) -> None:
if self._command:
self._command.undo()
# Client code
def main():
tv = TV()
remote = RemoteControl()
# Turn on TV
remote.set_command(TurnOnTVCommand(tv))
remote.press_button() # TV: On
remote.press_undo() # TV: Off
# Adjust volume
remote.set_command(VolumeUpCommand(tv))
remote.press_button() # TV: Volume +
remote.press_button() # TV: Volume +
remote.press_undo() # TV: Volume -
if __name__ == "__main__":
main()
Output:
TV: On
TV: Off
TV: Volume +
TV: Volume +
TV: Volume -
Iterator Pattern
Purpose: Provide a way to access elements of a collection sequentially without exposing its underlying structure (e.g., list, tree).
Use Case: A custom Playlist collection that allows iterating over songs without exposing its internal storage (e.g., a dictionary).
Python Example:
from collections.abc import Iterable, Iterator
# Custom collection
class Playlist(Iterable[str]):
def __init__(self):
self._songs: dict[int, str] = {} # Internal storage (hidden)
self._next_id = 1
def add_song(self, song: str) -> None:
self._songs[self._next_id] = song
self._next_id += 1
# Implement Iterable: return an iterator
def __iter__(self) -> Iterator[str]:
return PlaylistIterator(self._songs)
# Custom iterator
class PlaylistIterator(Iterator[str]):
def __init__(self, songs: dict[int, str]):
self._songs = songs
self._keys = iter(songs.keys()) # Iterate over sorted keys
def __next__(self) -> str:
key = next(self._keys)
return self._songs[key]
# Client code
def main():
playlist = Playlist()
playlist.add_song("Bohemian Rhapsody")
playlist.add_song("Hotel California")
playlist.add_song("Hey Jude")
# Iterate using the iterator
for song in playlist:
print(f"Playing: {song}")
if __name__ == "__main__":
main()
Output:
Playing: Bohemian Rhapsody
Playing: Hotel California
Playing: Hey Jude
State Pattern
Purpose: Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.
Use Case: A vending machine with states like “idle,” “selecting item,” and “dispensing.” Each state defines different behavior for user actions (e.g., inserting coins, selecting items).
Python Example:
from abc import ABC, abstractmethod
# Context: Vending Machine
class VendingMachine:
def __init__(self):
self._state: VendingMachineState = IdleState()
def set_state(self, state: "VendingMachineState") -> None:
self._state = state
def insert_coin(self) -> None:
self._state.insert_coin(self)
def select_item(self) -> None:
self._state.select_item(self)
def dispense(self) -> None:
self._state.dispense(self)
# State interface
class VendingMachineState(ABC):
@abstractmethod
def insert_coin(self, machine: VendingMachine) -> None:
pass
@abstractmethod
def select_item(self, machine: VendingMachine) -> None:
pass
@abstractmethod
def dispense(self, machine: VendingMachine) -> None:
pass
# Concrete states
class IdleState(VendingMachineState):
def insert_coin(self, machine: VendingMachine) -> None:
print("Coin inserted. Please select an item.")
machine.set_state(SelectingState())
def select_item(self, machine: VendingMachine) -> None:
print("Insert a coin first!")
def dispense(self, machine: VendingMachine) -> None:
print("Insert a coin and select an item first!")
class SelectingState(VendingMachineState):
def insert_coin(self, machine: VendingMachine) -> None:
print("Coin already inserted. Select an item.")
def select_item(self, machine: VendingMachine) -> None:
print("Item selected. Dispensing...")
machine.set_state(DispensingState())
def dispense(self, machine: VendingMachine) -> None:
print("Select an item first!")
class DispensingState(VendingMachineState):
def insert_coin(self, machine: VendingMachine) -> None:
print("Please wait, dispensing item...")
def select_item(self, machine: VendingMachine) -> None:
print("Please wait, dispensing item...")
def dispense(self, machine: VendingMachine) -> None:
print("Item dispensed!")
machine.set_state(IdleState())
# Client code
def main():
machine = VendingMachine()
machine.select_item() # Insert a coin first!
machine.insert_coin() # Coin inserted. Please select an item.
machine.insert_coin() # Coin already inserted. Select an item.
machine.select_item() # Item selected. Dispensing...
machine.dispense() # Item dispensed!
machine.insert_coin() # Coin inserted. Please select an item.
if __name__ == "__main__":
main()
Output:
Insert a coin first!
Coin inserted. Please select an item.
Coin already inserted. Select an item.
Item selected. Dispensing...
Item dispensed!
Coin inserted. Please select an item.
Structural vs. Behavioral: Key Differences
To summarize, here’s a comparison of Structural and Behavioral patterns:
| Aspect | Structural Patterns | Behavioral Patterns |
|---|---|---|
| Focus | Object/class composition and relationships | Object interaction and responsibility delegation |
| Primary Goal | Form larger, flexible structures from objects | Define algorithms, workflows, and communication |
| Key Concern | ”How to build” (structure) | “How to behave” (interaction) |
| Examples | Adapter, Decorator, Composite, Proxy, Facade | Observer, Strategy, Command, Iterator, State |
| Use Case Scenario | Integrating incompatible interfaces, adding features dynamically, simplifying complex systems | Event handling, algorithm selection, undo/redo, state management |
When to Use Which?
-
Structural Patterns are ideal when:
- You need to adapt existing code to a new interface (Adapter).
- You want to add features to objects dynamically (Decorator).
- You’re working with tree-like hierarchies (Composite).
- You need to control access to expensive resources (Proxy).
- You want to simplify a complex subsystem (Facade).
-
Behavioral Patterns are ideal when:
- Objects need to react to state changes (Observer).
- You want to switch algorithms at runtime (Strategy).
- You need to queue or undo requests (Command).
- You want to iterate over a collection without exposing its structure (Iterator).
- An object’s behavior depends on its state (State).
Conclusion
Structural and Behavioral design patterns solve distinct but complementary problems in software design. Structural patterns focus on composing objects into flexible structures, while Behavioral patterns manage interactions and responsibility delegation. By understanding their differences and use cases, you can select the right pattern to build robust, maintainable, and scalable Python applications.
Remember: Design patterns are tools, not rules. Always prioritize simplicity—use a pattern only if it solves a specific problem in your codebase.
References
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Python Design Patterns Documentation
- Real Python: Design Patterns in Python
- GeeksforGeeks: Structural Design Patterns
- GeeksforGeeks: Behavioral Design Patterns