Part 2: Structural Design Patterns
2 min read
Lesson slides
1 / 13
Purpose: Deal with object composition and relationships between entities.
2.1 Adapter Pattern
Intuition: Allows incompatible interfaces to work together. Like a power adapter that lets you plug your device into different outlet types.
classDiagram
class Target {
<<interface>>
+request()
}
class Adapter {
-adaptee: Adaptee
+request()
}
class Adaptee {
+specificRequest()
}
class Client {
+main()
}
Client --> Target
Adapter ..|> Target
Adapter --> Adaptee✅ When to Use:
- Integrating third-party libraries with different interfaces
- Legacy code integration without modification
- Making incompatible classes work together
- When you can't modify existing classes
- Creating reusable classes that work with unrelated classes
Code Example:
# Old system
class OldPrinter:
def old_print(self, text):
return f"Old printer: {text}"
# New interface expected by client
class ModernPrinter:
def print(self, text):
return f"Modern printer: {text}"
# Adapter to make old printer work with new interface
class PrinterAdapter:
def __init__(self, old_printer):
self.old_printer = old_printer
def print(self, text):
return self.old_printer.old_print(text)
# Client code
def client_code(printer):
return printer.print("Hello World")
# Usage
old_printer = OldPrinter()
adapter = PrinterAdapter(old_printer)
modern_printer = ModernPrinter()
print(client_code(adapter)) # Old printer: Hello World
print(client_code(modern_printer)) # Modern printer: Hello World2.2 Decorator Pattern
Intuition: Adds new functionality to objects dynamically without altering their structure. Like adding accessories to a phone case.
classDiagram
class Component {
<<interface>>
+operation()
}
class ConcreteComponent {
+operation()
}
class Decorator {
<<abstract>>
-component: Component
+operation()
}
class ConcreteDecoratorA {
+operation()
+addedBehavior()
}
class ConcreteDecoratorB {
+operation()
+addedState
}
Component <|.. ConcreteComponent
Component <|.. Decorator
Decorator <|-- ConcreteDecoratorA
Decorator <|-- ConcreteDecoratorB
Decorator --> Component✅ When to Use:
- Adding responsibilities to objects dynamically at runtime
- When subclassing would result in an explosion of classes
- Extending functionality without modifying existing code
- When you need different combinations of behaviors
- Creating configurable object behavior
Code Example:
from abc import ABC, abstractmethod
class Coffee(ABC):
@abstractmethod
def cost(self):
pass
@abstractmethod
def description(self):
pass
class SimpleCoffee(Coffee):
def cost(self):
return 2.0
def description(self):
return "Simple coffee"
class CoffeeDecorator(Coffee):
def __init__(self, coffee):
self._coffee = coffee
def cost(self):
return self._coffee.cost()
def description(self):
return self._coffee.description()
class Milk(CoffeeDecorator):
def cost(self):
return self._coffee.cost() + 0.5
def description(self):
return self._coffee.description() + ", milk"
class Sugar(CoffeeDecorator):
def cost(self):
return self._coffee.cost() + 0.2
def description(self):
return self._coffee.description() + ", sugar"
# Usage
coffee = SimpleCoffee()
coffee_with_milk = Milk(coffee)
coffee_with_milk_and_sugar = Sugar(coffee_with_milk)
print(f"{coffee_with_milk_and_sugar.description()}: ${coffee_with_milk_and_sugar.cost()}")
# Output: Simple coffee, milk, sugar: $2.72.3 Facade Pattern
Intuition: Provides a simplified interface to a complex subsystem. Like a TV remote that hides the complexity of the TV's internal systems.
classDiagram
class Facade {
-subsystem1: SubsystemA
-subsystem2: SubsystemB
-subsystem3: SubsystemC
+operation()
}
class SubsystemA {
+operationA1()
+operationA2()
}
class SubsystemB {
+operationB1()
+operationB2()
}
class SubsystemC {
+operationC1()
+operationC2()
}
class Client {
+main()
}
Client --> Facade
Facade --> SubsystemA
Facade --> SubsystemB
Facade --> SubsystemC✅ When to Use:
- Simplifying complex APIs or subsystems
- Providing a unified interface to a set of interfaces
- Decoupling clients from complex subsystems
- Creating layers of abstraction
- When you want to hide implementation details
Code Example:
# Complex subsystems
class DVDPlayer:
def on(self):
return "DVD Player is on"
def play(self, movie):
return f"Playing '{movie}'"
def off(self):
return "DVD Player is off"
class SoundSystem:
def on(self):
return "Sound System is on"
def set_volume(self, level):
return f"Volume set to {level}"
def off(self):
return "Sound System is off"
class Projector:
def on(self):
return "Projector is on"
def wide_screen_mode(self):
return "Projector in widescreen mode"
def off(self):
return "Projector is off"
# Facade
class HomeTheaterFacade:
def __init__(self):
self.dvd = DVDPlayer()
self.sound = SoundSystem()
self.projector = Projector()
def watch_movie(self, movie):
actions = [
self.projector.on(),
self.projector.wide_screen_mode(),
self.sound.on(),
self.sound.set_volume(5),
self.dvd.on(),
self.dvd.play(movie)
]
return actions
def end_movie(self):
actions = [
self.dvd.off(),
self.sound.off(),
self.projector.off()
]
return actions
# Usage
theater = HomeTheaterFacade()
print("Starting movie:")
for action in theater.watch_movie("The Matrix"):
print(f" - {action}")