Olox Olox

Theme

Documentation
Back to Home

Facade, Proxy, Bridge, Composite, Flyweight

8 min read

Structural Design Patterns (Part 2)


1️⃣ Facade Pattern

Intent: Provide a unified interface to a set of interfaces in a subsystem.

# Complex subsystem classes
class VideoConverter:
    def convert(self, filename: str, format: str) -> bytes:
        return f"Converting {filename} to {format}".encode()

class AudioExtractor:
    def extract(self, video: bytes) -> bytes:
        return b"extracted_audio"

class CodecFactory:
    def get_codec(self, format: str):
        return {"mp4": "H264", "avi": "MPEG"}.get(format, "RAW")

class BitrateReader:
    def read(self, video: bytes) -> int:
        return 128000

class Mixer:
    def mix(self, video: bytes, audio: bytes) -> bytes:
        return b"mixed_output"


# Facade
class VideoConversionFacade:
    """Simple interface to complex video conversion subsystem"""
    
    def __init__(self):
        self._converter = VideoConverter()
        self._audio = AudioExtractor()
        self._codec = CodecFactory()
        self._bitrate = BitrateReader()
        self._mixer = Mixer()
    
    def convert(self, filename: str, target_format: str) -> bytes:
        """One simple method hides all complexity"""
        print(f"Converting {filename} to {target_format}")
        
        # Complex internal operations
        codec = self._codec.get_codec(target_format)
        video = self._converter.convert(filename, target_format)
        audio = self._audio.extract(video)
        bitrate = self._bitrate.read(video)
        result = self._mixer.mix(video, audio)
        
        print(f"Used codec: {codec}, bitrate: {bitrate}")
        return result


# Client uses simple interface
facade = VideoConversionFacade()
result = facade.convert("movie.avi", "mp4")

2️⃣ Proxy Pattern

Intent: Provide a surrogate or placeholder for another object to control access.

Types of Proxy

from abc import ABC, abstractmethod
from typing import Optional
import time


# Subject interface
class Image(ABC):
    @abstractmethod
    def display(self) -> str:
        pass
    
    @abstractmethod
    def get_info(self) -> dict:
        pass


# Real Subject
class RealImage(Image):
    def __init__(self, filename: str):
        self.filename = filename
        self._load_from_disk()  # Expensive operation
    
    def _load_from_disk(self):
        print(f"Loading {self.filename} from disk...")
        time.sleep(0.1)  # Simulate slow load
        self.size = 1024 * 1024  # 1MB
    
    def display(self) -> str:
        return f"Displaying {self.filename}"
    
    def get_info(self) -> dict:
        return {"filename": self.filename, "size": self.size}


# Virtual Proxy (Lazy Loading)
class LazyImageProxy(Image):
    """Delays object creation until needed"""
    
    def __init__(self, filename: str):
        self.filename = filename
        self._real_image: Optional[RealImage] = None
    
    def _get_real_image(self) -> RealImage:
        if self._real_image is None:
            self._real_image = RealImage(self.filename)
        return self._real_image
    
    def display(self) -> str:
        return self._get_real_image().display()
    
    def get_info(self) -> dict:
        # Return basic info without loading
        return {"filename": self.filename, "loaded": self._real_image is not None}


# Protection Proxy (Access Control)
class ProtectedImageProxy(Image):
    """Controls access based on permissions"""
    
    def __init__(self, image: Image, user_role: str):
        self._image = image
        self._user_role = user_role
    
    def display(self) -> str:
        if self._user_role in ["admin", "viewer"]:
            return self._image.display()
        return "Access denied: insufficient permissions"
    
    def get_info(self) -> dict:
        return self._image.get_info()


# Caching Proxy
class CachingImageProxy(Image):
    """Caches results to avoid repeated operations"""
    
    _cache: dict = {}
    
    def __init__(self, filename: str):
        self.filename = filename
    
    def _get_image(self) -> RealImage:
        if self.filename not in self._cache:
            self._cache[self.filename] = RealImage(self.filename)
        else:
            print(f"Using cached image: {self.filename}")
        return self._cache[self.filename]
    
    def display(self) -> str:
        return self._get_image().display()
    
    def get_info(self) -> dict:
        return self._get_image().get_info()


# Logging Proxy
class LoggingImageProxy(Image):
    """Logs all access to the real object"""
    
    def __init__(self, image: Image):
        self._image = image
    
    def display(self) -> str:
        print(f"[LOG] display() called at {time.time()}")
        result = self._image.display()
        print(f"[LOG] display() returned: {result[:20]}...")
        return result
    
    def get_info(self) -> dict:
        print(f"[LOG] get_info() called")
        return self._image.get_info()


# Usage
print("=== Lazy Proxy ===")
lazy = LazyImageProxy("photo.jpg")
print(lazy.get_info())  # No loading
print(lazy.display())   # Loads here

print("\n=== Caching Proxy ===")
cached1 = CachingImageProxy("image.png")
cached2 = CachingImageProxy("image.png")
print(cached1.display())  # Loads
print(cached2.display())  # Uses cache

3️⃣ Bridge Pattern

Intent: Decouple an abstraction from its implementation so they can vary independently.

from abc import ABC, abstractmethod


# Implementation hierarchy
class Renderer(ABC):
    @abstractmethod
    def render_circle(self, x: int, y: int, radius: int) -> str:
        pass
    
    @abstractmethod
    def render_rectangle(self, x: int, y: int, w: int, h: int) -> str:
        pass


class VectorRenderer(Renderer):
    def render_circle(self, x: int, y: int, radius: int) -> str:
        return f"Drawing circle as vectors at ({x},{y}) r={radius}"
    
    def render_rectangle(self, x: int, y: int, w: int, h: int) -> str:
        return f"Drawing rectangle as vectors at ({x},{y}) {w}x{h}"


class RasterRenderer(Renderer):
    def render_circle(self, x: int, y: int, radius: int) -> str:
        return f"Drawing pixels for circle at ({x},{y}) r={radius}"
    
    def render_rectangle(self, x: int, y: int, w: int, h: int) -> str:
        return f"Drawing pixels for rectangle at ({x},{y}) {w}x{h}"


class SVGRenderer(Renderer):
    def render_circle(self, x: int, y: int, radius: int) -> str:
        return f'<circle cx="{x}" cy="{y}" r="{radius}"/>'
    
    def render_rectangle(self, x: int, y: int, w: int, h: int) -> str:
        return f'<rect x="{x}" y="{y}" width="{w}" height="{h}"/>'


# Abstraction hierarchy
class Shape(ABC):
    def __init__(self, renderer: Renderer):
        self.renderer = renderer  # Bridge to implementation
    
    @abstractmethod
    def draw(self) -> str:
        pass
    
    @abstractmethod
    def resize(self, factor: float) -> None:
        pass


class Circle(Shape):
    def __init__(self, renderer: Renderer, x: int, y: int, radius: int):
        super().__init__(renderer)
        self.x, self.y, self.radius = x, y, radius
    
    def draw(self) -> str:
        return self.renderer.render_circle(self.x, self.y, self.radius)
    
    def resize(self, factor: float) -> None:
        self.radius = int(self.radius * factor)


class Rectangle(Shape):
    def __init__(self, renderer: Renderer, x: int, y: int, w: int, h: int):
        super().__init__(renderer)
        self.x, self.y, self.w, self.h = x, y, w, h
    
    def draw(self) -> str:
        return self.renderer.render_rectangle(self.x, self.y, self.w, self.h)
    
    def resize(self, factor: float) -> None:
        self.w = int(self.w * factor)
        self.h = int(self.h * factor)


# Usage - shapes and renderers can vary independently
vector = VectorRenderer()
raster = RasterRenderer()
svg = SVGRenderer()

shapes = [
    Circle(vector, 10, 10, 5),
    Circle(svg, 20, 20, 10),
    Rectangle(raster, 0, 0, 100, 50),
]

for shape in shapes:
    print(shape.draw())

4️⃣ Composite Pattern

Intent: Compose objects into tree structures. Let clients treat individual objects and compositions uniformly.

from abc import ABC, abstractmethod
from typing import List


# Component
class FileSystemItem(ABC):
    def __init__(self, name: str):
        self.name = name
    
    @abstractmethod
    def get_size(self) -> int:
        pass
    
    @abstractmethod
    def display(self, indent: int = 0) -> str:
        pass
    
    def add(self, item: 'FileSystemItem') -> None:
        raise NotImplementedError("Cannot add to a file")
    
    def remove(self, item: 'FileSystemItem') -> None:
        raise NotImplementedError("Cannot remove from a file")


# Leaf
class File(FileSystemItem):
    def __init__(self, name: str, size: int):
        super().__init__(name)
        self.size = size
    
    def get_size(self) -> int:
        return self.size
    
    def display(self, indent: int = 0) -> str:
        return f"{'  ' * indent}📄 {self.name} ({self.size} bytes)"


# Composite
class Directory(FileSystemItem):
    def __init__(self, name: str):
        super().__init__(name)
        self.children: List[FileSystemItem] = []
    
    def add(self, item: FileSystemItem) -> None:
        self.children.append(item)
    
    def remove(self, item: FileSystemItem) -> None:
        self.children.remove(item)
    
    def get_size(self) -> int:
        return sum(child.get_size() for child in self.children)
    
    def display(self, indent: int = 0) -> str:
        lines = [f"{'  ' * indent}📁 {self.name}/"]
        for child in self.children:
            lines.append(child.display(indent + 1))
        return '\n'.join(lines)


# Build file system tree
root = Directory("root")
home = Directory("home")
user = Directory("user")

user.add(File("document.txt", 1024))
user.add(File("photo.jpg", 2048000))

downloads = Directory("downloads")
downloads.add(File("movie.mp4", 1500000000))
downloads.add(File("song.mp3", 5000000))

user.add(downloads)
home.add(user)
root.add(home)

root.add(Directory("etc"))
root.add(File("README.md", 512))

print(root.display())
print(f"\nTotal size: {root.get_size():,} bytes")

5️⃣ Flyweight Pattern

Intent: Use sharing to support large numbers of fine-grained objects efficiently.

from typing import Dict, Tuple
from dataclasses import dataclass


# Flyweight (intrinsic state - shared)
@dataclass(frozen=True)
class CharacterStyle:
    """Shared character formatting"""
    font: str
    size: int
    bold: bool
    italic: bool


# Flyweight Factory
class StyleFactory:
    _styles: Dict[Tuple, CharacterStyle] = {}
    
    @classmethod
    def get_style(cls, font: str, size: int, bold: bool = False, 
                  italic: bool = False) -> CharacterStyle:
        key = (font, size, bold, italic)
        
        if key not in cls._styles:
            cls._styles[key] = CharacterStyle(font, size, bold, italic)
            print(f"Created new style: {key}")
        
        return cls._styles[key]
    
    @classmethod
    def style_count(cls) -> int:
        return len(cls._styles)


# Context (extrinsic state - unique per character)
@dataclass
class Character:
    char: str
    x: int
    y: int
    style: CharacterStyle  # Flyweight reference
    
    def render(self) -> str:
        return f"'{self.char}' at ({self.x},{self.y}) [{self.style.font} {self.style.size}pt]"


# Document using flyweight
class Document:
    def __init__(self):
        self.characters: list[Character] = []
    
    def add_character(self, char: str, x: int, y: int, 
                     font: str, size: int, bold: bool = False, 
                     italic: bool = False):
        style = StyleFactory.get_style(font, size, bold, italic)
        self.characters.append(Character(char, x, y, style))
    
    def render(self) -> str:
        return '\n'.join(c.render() for c in self.characters[:5]) + "..."


# Usage - many characters share few styles
doc = Document()

text = "Hello World! This is a test document with many characters."
x = 0
for char in text:
    doc.add_character(char, x, 0, "Arial", 12)
    x += 10

# Add some bold text
for i, char in enumerate("IMPORTANT"):
    doc.add_character(char, i * 12, 20, "Arial", 14, bold=True)

print(doc.render())
print(f"\nTotal characters: {len(doc.characters)}")
print(f"Unique styles: {StyleFactory.style_count()}")

Game Example - Tree Forest

from dataclasses import dataclass
from typing import Dict, List
import random


# Flyweight - shared tree type data
@dataclass(frozen=True)
class TreeType:
    name: str
    color: str
    texture: str  # In real app, this would be heavy texture data
    
    def draw(self, x: int, y: int) -> str:
        return f"{self.name} tree at ({x},{y})"


# Flyweight Factory
class TreeFactory:
    _types: Dict[str, TreeType] = {}
    
    @classmethod
    def get_tree_type(cls, name: str, color: str, texture: str) -> TreeType:
        key = f"{name}_{color}_{texture}"
        
        if key not in cls._types:
            cls._types[key] = TreeType(name, color, texture)
        
        return cls._types[key]
    
    @classmethod
    def type_count(cls) -> int:
        return len(cls._types)


# Context - individual tree with position
@dataclass
class Tree:
    x: int
    y: int
    tree_type: TreeType
    
    def draw(self) -> str:
        return self.tree_type.draw(self.x, self.y)


# Forest containing many trees
class Forest:
    def __init__(self):
        self.trees: List[Tree] = []
    
    def plant_tree(self, x: int, y: int, name: str, color: str, texture: str):
        tree_type = TreeFactory.get_tree_type(name, color, texture)
        self.trees.append(Tree(x, y, tree_type))
    
    def draw(self) -> str:
        return f"Forest with {len(self.trees)} trees"


# Create large forest efficiently
forest = Forest()

tree_specs = [
    ("Oak", "green", "oak_bark"),
    ("Pine", "dark_green", "pine_bark"),
    ("Birch", "light_green", "birch_bark"),
]

# Plant 10,000 trees but only 3 tree types
for _ in range(10000):
    name, color, texture = random.choice(tree_specs)
    x, y = random.randint(0, 1000), random.randint(0, 1000)
    forest.plant_tree(x, y, name, color, texture)

print(forest.draw())
print(f"Memory efficient: Only {TreeFactory.type_count()} tree types for 10,000 trees")

🔄 Pattern Comparison

PatternPurposeKey Mechanism
AdapterInterface compatibilityWraps with different interface
BridgeAbstraction/impl separationComposition over inheritance
CompositeTree structuresUniform component interface
DecoratorAdd behavior dynamicallyWraps with same interface
FacadeSimplify subsystemSingle entry point
FlyweightMemory optimizationShare common state
ProxyControl accessSurrogate object

Last Updated: 2024