Composition Pattern¶
Mental Model
Composition means an object owns its parts and controls their lifetime -- when the owner dies, the parts die with it. Think of a car and its engine: the engine is created inside the car and has no independent existence. This tight ownership distinguishes composition from looser forms of association like aggregation.
Strong Has-a¶
1. Definition¶
Composition is a strong form of association where:
- One class owns instances of another class
- The lifetime of the component is tied to the container
- When the container is destroyed, components are destroyed
- Models whole-part relationships
2. Ownership Model¶
The container creates and controls its components:
```python class Engine: def start(self): return "Engine running"
class Car: def init(self): self.engine = Engine() # Created here, owned by Car
def start(self):
return self.engine.start()
car = Car() # Creates Engine del car # Destroys Engine too ```
3. Key Principle¶
Components cannot exist meaningfully outside their container.
Implementation¶
1. Basic Pattern¶
```python class Heart: def beat(self): return "Beating"
class Brain: def think(self): return "Thinking"
class Human: def init(self, name): self.name = name self.heart = Heart() # Composed self.brain = Brain() # Composed
def live(self):
return f"{self.heart.beat()}, {self.brain.think()}"
person = Human("Alice") print(person.live()) # Beating, Thinking ```
2. Multiple Components¶
```python class CPU: def init(self, cores): self.cores = cores
def process(self):
return f"Processing on {self.cores} cores"
class RAM: def init(self, size): self.size = size
def store(self):
return f"Storing in {self.size}GB RAM"
class Computer: def init(self, cpu_cores, ram_size): self.cpu = CPU(cpu_cores) # Composed self.ram = RAM(ram_size) # Composed self.storage = [] # Composed
def specs(self):
return f"{self.cpu.process()}, {self.ram.store()}"
pc = Computer(8, 16) print(pc.specs()) # Processing on 8 cores, Storing in 16GB RAM ```
3. Nested Composition¶
```python class Wheel: def init(self, size): self.size = size
class Engine: def init(self, horsepower): self.horsepower = horsepower
class Chassis: def init(self): self.wheels = [Wheel(18) for _ in range(4)]
class Car: def init(self): self.chassis = Chassis() # Composed self.engine = Engine(200) # Composed ```
Benefits¶
1. Modularity¶
Break complex systems into manageable parts:
```python class DisplayController: def render(self): return "Rendering display"
class InputController: def handle_input(self): return "Handling input"
class AudioController: def play_sound(self): return "Playing sound"
class GameConsole: def init(self): self.display = DisplayController() self.input = InputController() self.audio = AudioController()
def run(self):
return f"{self.display.render()}, {self.input.handle_input()}"
```
2. Encapsulation¶
Hide implementation details:
```python class Database: def query(self, sql): # Complex database logic return "Query result"
class Cache: def get(self, key): return "Cached value"
class DataService: def init(self): self._database = Database() # Hidden implementation self._cache = Cache() # Hidden implementation
def get_data(self, id):
# Client doesn't know about DB or Cache
cached = self._cache.get(id)
if cached:
return cached
return self._database.query(f"SELECT * FROM table WHERE id={id}")
```
3. Flexibility¶
Swap implementations without changing interface:
```python class EmailSender: def send(self, message): return f"Email sent: {message}"
class SMSSender: def send(self, message): return f"SMS sent: {message}"
class NotificationService: def init(self, sender): self.sender = sender
def notify(self, message):
return self.sender.send(message)
Flexible composition¶
email_service = NotificationService(EmailSender()) sms_service = NotificationService(SMSSender()) ```
Lifetime Control¶
1. Container Controls¶
The container manages component lifecycle:
```python class Connection: def init(self): print("Connection opened")
def close(self):
print("Connection closed")
class DatabaseSession: def init(self): self.connection = Connection() # Created
def __del__(self):
self.connection.close() # Destroyed with container
session = DatabaseSession() # Connection opened del session # Connection closed ```
2. Initialization Order¶
Components initialize in order:
```python class Logger: def init(self): print("Logger initialized")
class Config: def init(self): print("Config initialized")
class Application: def init(self): self.config = Config() # First self.logger = Logger() # Second print("Application ready")
app = Application()
Output:¶
Config initialized¶
Logger initialized¶
Application ready¶
```
3. Cleanup Guarantee¶
Container ensures cleanup:
```python class TempFile: def init(self, name): self.name = name print(f"Created {name}")
def cleanup(self):
print(f"Deleted {name}")
class FileProcessor: def init(self): self.temp_files = [TempFile(f"temp_{i}") for i in range(3)]
def __del__(self):
for f in self.temp_files:
f.cleanup()
processor = FileProcessor()
Created temp_0, temp_1, temp_2¶
del processor
Deleted temp_0, temp_1, temp_2¶
```
Design Patterns¶
1. Builder Pattern¶
```python class Burger: def init(self): self.bun = None self.patty = None self.toppings = []
class BurgerBuilder: def init(self): self.burger = Burger()
def add_bun(self, bun_type):
self.burger.bun = bun_type
return self
def add_patty(self, patty_type):
self.burger.patty = patty_type
return self
def add_topping(self, topping):
self.burger.toppings.append(topping)
return self
def build(self):
return self.burger
burger = (BurgerBuilder() .add_bun("sesame") .add_patty("beef") .add_topping("lettuce") .add_topping("tomato") .build()) ```
2. Facade Pattern¶
```python class VideoFile: def load(self): return "Video loaded"
class AudioFile: def load(self): return "Audio loaded"
class Codec: def decode(self): return "Decoded"
class MediaPlayer: """Facade that composes subsystems""" def init(self): self.video = VideoFile() self.audio = AudioFile() self.codec = Codec()
def play(self, filename):
video = self.video.load()
audio = self.audio.load()
decoded = self.codec.decode()
return f"{video}, {audio}, {decoded}"
player = MediaPlayer() player.play("movie.mp4") # Simple interface, complex composition ```
3. Composite Pattern¶
```python class File: def init(self, name, size): self.name = name self.size = size
def get_size(self):
return self.size
class Directory: def init(self, name): self.name = name self.children = []
def add(self, item):
self.children.append(item)
def get_size(self):
return sum(child.get_size() for child in self.children)
root = Directory("root") root.add(File("file1.txt", 100)) root.add(File("file2.txt", 200))
subdir = Directory("subdir") subdir.add(File("file3.txt", 150)) root.add(subdir)
print(root.get_size()) # 450 ```
Common Use Cases¶
1. Document Structure¶
```python class Paragraph: def init(self, text): self.text = text
class Image: def init(self, url): self.url = url
class Section: def init(self, title): self.title = title self.paragraphs = [] self.images = []
class Document: def init(self, title): self.title = title self.sections = []
def add_section(self, section):
self.sections.append(section)
```
2. UI Components¶
```python class Label: def init(self, text): self.text = text
class Button: def init(self, label): self.label = label
class TextInput: def init(self, placeholder): self.placeholder = placeholder
class Form: def init(self): self.inputs = [] self.buttons = [] self.labels = []
def add_input(self, input_field):
self.inputs.append(input_field)
```
3. Game Objects¶
```python class Position: def init(self, x, y): self.x = x self.y = y
class Sprite: def init(self, image): self.image = image
class Collider: def init(self, width, height): self.width = width self.height = height
class GameObject: def init(self): self.position = Position(0, 0) self.sprite = Sprite("default.png") self.collider = Collider(32, 32) ```
Best Practices¶
1. Single Responsibility¶
Each component has one job:
```python class Validator: def validate(self, data): return True
class Parser: def parse(self, text): return {"parsed": text}
class Processor: def init(self): self.validator = Validator() self.parser = Parser()
def process(self, text):
if self.validator.validate(text):
return self.parser.parse(text)
```
2. Dependency Injection¶
Pass dependencies rather than create:
```python
✅ GOOD - Flexible¶
class Service: def init(self, logger, database): self.logger = logger self.database = database
❌ BAD - Rigid¶
class Service: def init(self): self.logger = Logger() # Hard-coded self.database = Database() # Hard-coded ```
3. Interface Segregation¶
Depend on abstractions:
```python from abc import ABC, abstractmethod
class Storage(ABC): @abstractmethod def save(self, data): pass
class FileStorage(Storage): def save(self, data): # Save to file pass
class DataManager: def init(self, storage: Storage): self.storage = storage # Depends on interface ```
Testing¶
1. Easy Mocking¶
Composition enables easy testing:
```python class MockDatabase: def query(self, sql): return {"mock": "data"}
class DataService: def init(self, database): self.database = database
def get_user(self, id):
return self.database.query(f"SELECT * FROM users WHERE id={id}")
Easy to test¶
service = DataService(MockDatabase()) assert service.get_user(1) == {"mock": "data"} ```
2. Isolated Testing¶
Test components independently:
```python class Calculator: def add(self, a, b): return a + b
class Display: def show(self, value): return f"Result: {value}"
class CalculatorApp: def init(self): self.calc = Calculator() self.display = Display()
Test Calculator alone¶
assert Calculator().add(2, 3) == 5
Test Display alone¶
assert Display().show(5) == "Result: 5" ```
3. Integration Testing¶
Test composed system:
python
app = CalculatorApp()
result = app.calc.add(2, 3)
output = app.display.show(result)
assert output == "Result: 5"
Exercises¶
Exercise 1.
Create a Computer class that owns an CPU and a Memory component (composition). The CPU has a process(data) method, and Memory has read() and write(data) methods. The Computer's run(data) method should write data to memory, process it with the CPU, and return the result. Demonstrate that the components do not exist independently.
Solution to Exercise 1
class CPU:
def process(self, data):
return data * 2
class Memory:
def __init__(self):
self._data = None
def write(self, data):
self._data = data
def read(self):
return self._data
class Computer:
def __init__(self):
self.cpu = CPU() # Composition: Computer owns CPU
self.memory = Memory() # Composition: Computer owns Memory
def run(self, data):
self.memory.write(data)
stored = self.memory.read()
return self.cpu.process(stored)
pc = Computer()
print(pc.run(21)) # 42
Exercise 2.
Implement a House class that is composed of Room objects. Each Room has a name and area. The House should provide a total_area() method that sums all room areas, and an add_room(name, area) method. Demonstrate that rooms are created by the house and cannot outlive it.
Solution to Exercise 2
class Room:
def __init__(self, name, area):
self.name = name
self.area = area
class House:
def __init__(self):
self._rooms = []
def add_room(self, name, area):
room = Room(name, area) # House creates the Room
self._rooms.append(room)
def total_area(self):
return sum(r.area for r in self._rooms)
house = House()
house.add_room("Kitchen", 20)
house.add_room("Bedroom", 15)
house.add_room("Living Room", 30)
print(f"Total area: {house.total_area()} sqm") # 65
Exercise 3.
Build a ReportBuilder using the builder pattern with composition. The ReportBuilder should have add_title(text), add_paragraph(text), and build() methods. Each call to add_title or add_paragraph stores a component internally. build() returns the full report as a single string with titles in uppercase and paragraphs as-is, separated by newlines.
Solution to Exercise 3
class ReportBuilder:
def __init__(self):
self._parts = []
def add_title(self, text):
self._parts.append(("title", text))
return self
def add_paragraph(self, text):
self._parts.append(("paragraph", text))
return self
def build(self):
lines = []
for kind, text in self._parts:
if kind == "title":
lines.append(text.upper())
else:
lines.append(text)
return "\n".join(lines)
report = (
ReportBuilder()
.add_title("Introduction")
.add_paragraph("This is the first paragraph.")
.add_title("Conclusion")
.add_paragraph("This wraps up the report.")
.build()
)
print(report)