Structural patterns deal with object composition, creating relationships between objects to form larger structures while keeping these structures flexible and efficient.
Adapter
Problem: Need to make incompatible interfaces work together.
When to use:
- Want to use an existing class with incompatible interface
- Need to create reusable class that cooperates with unrelated classes
- Need to use several existing subclasses, but impractical to adapt by subclassing each one
Structure:
- Target defines the interface client uses
- Adaptee has incompatible interface
- Adapter makes Adaptee work with Target
Example (TypeScript):
// Target interface
interface MediaPlayer {
play(filename: string): void;
}
// Adaptee with incompatible interface
class VLCPlayer {
playVLC(filename: string) {
console.log(`Playing VLC file: ${filename}`);
}
}
// Adapter
class MediaAdapter implements MediaPlayer {
constructor(private vlc: VLCPlayer) {}
play(filename: string): void {
this.vlc.playVLC(filename);
}
}
// Client
class AudioPlayer implements MediaPlayer {
play(filename: string): void {
if (filename.endsWith('.mp3')) {
console.log(`Playing MP3: ${filename}`);
} else if (filename.endsWith('.vlc')) {
const adapter = new MediaAdapter(new VLCPlayer());
adapter.play(filename);
}
}
}
// Usage
const player = new AudioPlayer();
player.play('song.mp3');
player.play('movie.vlc');Example (Ruby):
# Target interface
class MediaPlayer
def play(filename)
raise NotImplementedError
end
end
# Adaptee
class VLCPlayer
def play_vlc(filename)
puts "Playing VLC file: #{filename}"
end
end
# Adapter
class MediaAdapter < MediaPlayer
def initialize(vlc_player)
@vlc = vlc_player
end
def play(filename)
@vlc.play_vlc(filename)
end
end
# Client
class AudioPlayer < MediaPlayer
def play(filename)
if filename.end_with?('.mp3')
puts "Playing MP3: #{filename}"
elsif filename.end_with?('.vlc')
adapter = MediaAdapter.new(VLCPlayer.new)
adapter.play(filename)
end
end
endBridge
Problem: Need to decouple abstraction from implementation so both can vary independently.
When to use:
- Want to avoid permanent binding between abstraction and implementation
- Both abstractions and implementations should be extensible by subclassing
- Changes in implementation shouldn’t affect clients
- Want to share implementation among multiple objects
Structure:
- Abstraction defines high-level operations
- Implementor defines low-level operations
- Refined abstractions extend abstraction
- Concrete implementors implement low-level operations
Example (TypeScript):
// Implementor
interface Renderer {
renderCircle(radius: number): void;
renderSquare(side: number): void;
}
// Concrete implementors
class VectorRenderer implements Renderer {
renderCircle(radius: number) {
console.log(`Drawing circle (vector) with radius ${radius}`);
}
renderSquare(side: number) {
console.log(`Drawing square (vector) with side ${side}`);
}
}
class RasterRenderer implements Renderer {
renderCircle(radius: number) {
console.log(`Drawing circle (raster) with radius ${radius}`);
}
renderSquare(side: number) {
console.log(`Drawing square (raster) with side ${side}`);
}
}
// Abstraction
abstract class Shape {
constructor(protected renderer: Renderer) {}
abstract draw(): void;
}
// Refined abstractions
class Circle extends Shape {
constructor(renderer: Renderer, private radius: number) {
super(renderer);
}
draw() {
this.renderer.renderCircle(this.radius);
}
}
class Square extends Shape {
constructor(renderer: Renderer, private side: number) {
super(renderer);
}
draw() {
this.renderer.renderSquare(this.side);
}
}
// Usage
const vectorCircle = new Circle(new VectorRenderer(), 5);
const rasterSquare = new Square(new RasterRenderer(), 10);
vectorCircle.draw();
rasterSquare.draw();Composite
Problem: Need to treat individual objects and compositions of objects uniformly.
When to use:
- Want to represent part-whole hierarchies
- Want clients to treat individual and composite objects uniformly
- Structure can have arbitrary depth
Structure:
- Component declares interface for objects in composition
- Leaf represents individual objects
- Composite stores child components and implements operations
Example (TypeScript):
interface Graphic {
draw(): void;
}
class Circle implements Graphic {
draw() {
console.log('Drawing circle');
}
}
class Square implements Graphic {
draw() {
console.log('Drawing square');
}
}
class CompositeGraphic implements Graphic {
private children: Graphic[] = [];
add(graphic: Graphic) {
this.children.push(graphic);
}
remove(graphic: Graphic) {
const index = this.children.indexOf(graphic);
if (index > -1) {
this.children.splice(index, 1);
}
}
draw() {
console.log('Drawing composite:');
this.children.forEach(child => child.draw());
}
}
// Usage
const graphic = new CompositeGraphic();
graphic.add(new Circle());
graphic.add(new Square());
const subGraphic = new CompositeGraphic();
subGraphic.add(new Circle());
graphic.add(subGraphic);
graphic.draw();Decorator
Problem: Need to add responsibilities to objects dynamically without affecting other objects.
When to use:
- Add responsibilities to individual objects dynamically
- Responsibilities can be withdrawn
- Extension by subclassing is impractical
- Need flexible alternative to subclassing
Structure:
- Component defines interface
- Concrete component implements base behavior
- Decorator wraps component and adds behavior
Example (TypeScript):
interface Coffee {
cost(): number;
description(): string;
}
class SimpleCoffee implements Coffee {
cost() { return 5; }
description() { return 'Simple coffee'; }
}
class CoffeeDecorator implements Coffee {
constructor(protected coffee: Coffee) {}
cost() { return this.coffee.cost(); }
description() { return this.coffee.description(); }
}
class MilkDecorator extends CoffeeDecorator {
cost() { return this.coffee.cost() + 2; }
description() { return this.coffee.description() + ', milk'; }
}
class SugarDecorator extends CoffeeDecorator {
cost() { return this.coffee.cost() + 1; }
description() { return this.coffee.description() + ', sugar'; }
}
// Usage
let coffee: Coffee = new SimpleCoffee();
console.log(`${coffee.description()}: $${coffee.cost()}`);
coffee = new MilkDecorator(coffee);
console.log(`${coffee.description()}: $${coffee.cost()}`);
coffee = new SugarDecorator(coffee);
console.log(`${coffee.description()}: $${coffee.cost()}`);Example (Ruby):
class Coffee
def cost
raise NotImplementedError
end
def description
raise NotImplementedError
end
end
class SimpleCoffee < Coffee
def cost
5
end
def description
'Simple coffee'
end
end
class CoffeeDecorator < Coffee
def initialize(coffee)
@coffee = coffee
end
def cost
@coffee.cost
end
def description
@coffee.description
end
end
class MilkDecorator < CoffeeDecorator
def cost
@coffee.cost + 2
end
def description
"#{@coffee.description}, milk"
end
end
class SugarDecorator < CoffeeDecorator
def cost
@coffee.cost + 1
end
def description
"#{@coffee.description}, sugar"
end
end
# Usage
coffee = SimpleCoffee.new
coffee = MilkDecorator.new(coffee)
coffee = SugarDecorator.new(coffee)
puts "#{coffee.description}: $#{coffee.cost}"Facade
Problem: Need to provide a simplified interface to a complex subsystem.
When to use:
- Want to provide simple interface to complex subsystem
- Many dependencies between clients and implementation classes
- Want to layer subsystems
Structure:
- Facade provides convenient methods for common tasks
- Subsystem classes implement functionality
- Clients use facade instead of subsystem directly
Example (TypeScript):
// Complex subsystem
class CPU {
freeze() { console.log('CPU: Freezing'); }
jump(position: number) { console.log(`CPU: Jump to ${position}`); }
execute() { console.log('CPU: Executing'); }
}
class Memory {
load(position: number, data: string) {
console.log(`Memory: Load ${data} at ${position}`);
}
}
class HardDrive {
read(sector: number, size: number): string {
console.log(`HDD: Read sector ${sector}, size ${size}`);
return 'boot data';
}
}
// Facade
class ComputerFacade {
constructor(
private cpu: CPU,
private memory: Memory,
private hdd: HardDrive
) {}
start() {
console.log('Starting computer...');
this.cpu.freeze();
const bootData = this.hdd.read(0, 1024);
this.memory.load(0, bootData);
this.cpu.jump(0);
this.cpu.execute();
console.log('Computer started!');
}
}
// Usage
const computer = new ComputerFacade(
new CPU(),
new Memory(),
new HardDrive()
);
computer.start();Flyweight
Problem: Need to support large numbers of fine-grained objects efficiently by sharing.
When to use:
- Application uses large number of objects
- Storage costs are high due to quantity
- Most object state can be made extrinsic
- Many groups of objects can be replaced by fewer shared objects
Structure:
- Flyweight stores intrinsic (shared) state
- Context stores extrinsic (unique) state
- Factory manages flyweight pool
Example (TypeScript):
// Flyweight
class TreeType {
constructor(
public name: string,
public color: string,
public texture: string
) {}
draw(x: number, y: number) {
console.log(`Drawing ${this.name} tree at (${x}, ${y})`);
}
}
// Flyweight factory
class TreeFactory {
private static types: Map<string, TreeType> = new Map();
static getTreeType(name: string, color: string, texture: string): TreeType {
const key = `${name}-${color}-${texture}`;
if (!this.types.has(key)) {
this.types.set(key, new TreeType(name, color, texture));
}
return this.types.get(key)!;
}
static getTypeCount() {
return this.types.size;
}
}
// Context (stores extrinsic state)
class Tree {
constructor(
private x: number,
private y: number,
private type: TreeType
) {}
draw() {
this.type.draw(this.x, this.y);
}
}
// Forest uses flyweight pattern
class Forest {
private trees: Tree[] = [];
plantTree(x: number, y: number, name: string, color: string, texture: string) {
const type = TreeFactory.getTreeType(name, color, texture);
const tree = new Tree(x, y, type);
this.trees.push(tree);
}
draw() {
this.trees.forEach(tree => tree.draw());
}
}
// Usage
const forest = new Forest();
forest.plantTree(1, 2, 'Oak', 'Green', 'Rough');
forest.plantTree(3, 4, 'Oak', 'Green', 'Rough'); // Reuses type
forest.plantTree(5, 6, 'Pine', 'Dark Green', 'Smooth');
forest.draw();
console.log(`Total tree types: ${TreeFactory.getTypeCount()}`); // 2, not 3Proxy
Problem: Need to control access to an object with a surrogate or placeholder.
When to use:
- Need lazy initialization (virtual proxy)
- Need access control (protection proxy)
- Need local representative for remote object (remote proxy)
- Need to add wrapper before/after operations (smart reference)
Structure:
- Subject defines common interface
- Real subject implements actual functionality
- Proxy controls access to real subject
Example (TypeScript):
interface Image {
display(): void;
}
class RealImage implements Image {
constructor(private filename: string) {
this.loadFromDisk();
}
private loadFromDisk() {
console.log(`Loading ${this.filename} from disk...`);
}
display() {
console.log(`Displaying ${this.filename}`);
}
}
class ImageProxy implements Image {
private realImage?: RealImage;
constructor(private filename: string) {}
display() {
// Lazy loading: only create real image when needed
if (!this.realImage) {
this.realImage = new RealImage(this.filename);
}
this.realImage.display();
}
}
// Usage
const image = new ImageProxy('photo.jpg');
console.log('Image proxy created');
// Image not loaded yet
image.display(); // Now it loads
image.display(); // Uses cached instanceProtection Proxy Example:
interface Document {
read(): string;
write(content: string): void;
}
class RealDocument implements Document {
private content: string = '';
read(): string {
return this.content;
}
write(content: string): void {
this.content = content;
}
}
class ProtectedDocument implements Document {
constructor(
private document: RealDocument,
private userRole: string
) {}
read(): string {
return this.document.read();
}
write(content: string): void {
if (this.userRole !== 'admin') {
throw new Error('Access denied: only admins can write');
}
this.document.write(content);
}
}
// Usage
const doc = new RealDocument();
const userDoc = new ProtectedDocument(doc, 'user');
userDoc.read(); // OK
// userDoc.write('test'); // Error: Access denied
const adminDoc = new ProtectedDocument(doc, 'admin');
adminDoc.write('Admin content'); // OKRelated Patterns
- Creational Patterns - Often create objects used in structural patterns
- Behavioral Patterns - Define how objects interact
- Design Patterns Overview