Composite & Decorator Patterns
Continuing our journey through Structural Patterns, we look at two patterns that help manage the relationship between objects and their compositions. While both involve wrapping or containing objects, they serve very different architectural purposes.
1. The Composite Pattern
The Problem
Imagine you are building a file system representation. You have File objects and Folder objects. Folders can contain both Files and other Folders. If you want to calculate the total size of a Folder, you have to check every item: if it’s a File, add its size; if it’s a Folder, recursively call its size method. This makes the client code complex as it must treat individual objects and groups of objects differently.
The Solution
The Composite pattern allows you to compose objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions of objects uniformly.
By defining a common interface for both the simple elements (Leaves) and the containers (Composites), the client can interact with any object in the tree without knowing its exact concrete class.
Java Implementation
import java.util.ArrayList;
import java.util.List;
// 1. Component Interface
interface FileSystemComponent {
void showDetails();
int getSize();
}
// 2. Leaf Object
class File implements FileSystemComponent {
private String name;
private int size;
public File(String name, int size) {
this.name = name;
this.size = size;
}
public void showDetails() {
System.out.println("File: " + name + " (" + size + "KB)");
}
public int getSize() {
return size;
}
}
// 3. Composite Object
class Folder implements FileSystemComponent {
private String name;
private List<FileSystemComponent> components = new ArrayList<>();
public Folder(String name) {
this.name = name;
}
public void addComponent(FileSystemComponent component) {
components.add(component);
}
public void showDetails() {
System.out.println("Folder: " + name);
for (FileSystemComponent component : components) {
component.showDetails();
}
}
public int getSize() {
int totalSize = 0;
for (FileSystemComponent component : components) {
totalSize += component.getSize();
}
return totalSize;
}
}
Pros and Cons
- Pros: Simplifies client code by treating complex tree structures as single objects. Makes it easy to add new types of components.
- Cons: Can make the design overly general; it might be difficult to restrict which components can be added to a folder at compile time.
2. The Decorator Pattern
The Problem
Suppose you have a Coffee class. You want to offer options like Milk, Sugar, or WhippedCream. If you use inheritance, you end up with a “class explosion”: CoffeeWithMilk, CoffeeWithSugar, CoffeeWithMilkAndSugar, etc. Static inheritance is inflexible because you cannot add behavior at runtime.
The Solution
The Decorator pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
Instead of inheriting from the base class, you “wrap” the base object inside a decorator object that implements the same interface and adds its own behavior before or after delegating to the wrapped object.
Python Example
from abc import ABC, abstractmethod
# 1. Component Interface
class Coffee(ABC):
@abstractmethod
def get_cost(self):
pass
@abstractmethod
def get_description(self):
pass
# 2. Concrete Component
class SimpleCoffee(Coffee):
def get_cost(self):
return 2.0
def get_description(self):
return "Simple Coffee"
# 3. Base Decorator
class CoffeeDecorator(Coffee):
def __init__(self, coffee: Coffee):
self._decorated_coffee = coffee
def get_cost(self):
return self._decorated_coffee.get_cost()
def get_description(self):
return self._decorated_coffee.get_description()
# 4. Concrete Decorators
class MilkDecorator(CoffeeDecorator):
def get_cost(self):
return super().get_cost() + 0.5
def get_description(self):
return super().get_description() + ", Milk"
class SugarDecorator(CoffeeDecorator):
def get_cost(self):
return super().get_cost() + 0.2
def get_description(self):
return super().get_description() + ", Sugar"
# Usage
my_coffee = SimpleCoffee()
my_coffee = MilkDecorator(my_coffee)
my_coffee = SugarDecorator(my_coffee)
print(f"Order: {my_coffee.get_description()}")
print(f"Total: ${my_coffee.get_cost():.2f}")
Pros and Cons
- Pros: Greater flexibility than static inheritance. Allows for “mix-and-match” behavior at runtime. Adheres to the Single Responsibility Principle (each decorator handles one feature).
- Cons: Can result in many small objects that look alike, making debugging harder. The order of decorators might matter, which can be a source of bugs.