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
end

Bridge

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 3

Proxy

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 instance

Protection 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'); // OK

See Also