Logo

Part 2: Structural Design Patterns

2 min read
Lesson slides
1 / 13

Part 2: Structural Design Patterns

Adapter, Decorator, and Facade

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 World

2.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.7

2.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}")