Skip to content

Structural pattern

Structural patterns are another category of design patterns, and they focus on how classes and objects are composed to form larger structures while keeping the system flexible and efficient.

Goals

  • Organize code by creating relationships between objects or classes.
  • Make it easier to change or extend systems without breaking everything.

Simplify complex structures into more manageable components.

Adapter

A design pattern that allows objects with incompatible interfaces to work together. It acts like a bridge between two incompatible interfaces by wrapping an existing class with a new interface.

Key features

Client → Target Interface

         Adapter

      Adaptee (incompatible class)

Target: The interface your code expects.

Adaptee: The existing class with an incompatible interface.

Adapter: A wrapper that makes Adaptee compatible with Target.

  • Allows reuse of existing classes without modifying them.
  • Promotes flexibility and reusability.

Cons

  • Can add extra layers and complexity.
  • Overuse can lead to messy or hard-to-follow code.

Bridge

Used to decouple an abstraction from its implementation, so the two can vary independently.

Key features

Imagine you have multiple abstractions (like RemoteControl) and multiple implementations (like TV, Radio). Instead of creating a new class for every combination (e.g., AdvancedRemoteForTV, BasicRemoteForRadio, etc.), the Bridge Pattern separates the two hierarchies — abstraction and implementation — and connects them with a bridge.

Think of it like a TV remote: the remote (abstraction) can work with different brands of TVs (implementations) without being tightly coupled to any one of them.

Abstraction
  └── RefinedAbstraction

     Implementor (interface)
        └── ConcreteImplementorA
        └── ConcreteImplementorB

Abstraction: High-level control layer (e.g., RemoteControl)

Implementor: Interface for lower-level operations (e.g., Device)

ConcreteImplementor: Specific implementations (e.g., TV, Radio)

Bridge: The abstraction holds a reference to an implementor

  • Decouples abstraction and implementation
  • Increases flexibility when changing or extending systems
  • Reduces class explosion from multiple inheritance combinations

Cons

  • Adds complexity and indirection
  • Might be overkill for simple scenarios

Composite

Lets you treat individual objects and compositions of objects uniformly.

E.g. building a system with hierarchical structures, like a file system. You want to be able to treat both individual items (like files) and groups of items (like folders) in the same way.

Component (interface)
├── Leaf (single object)
└── Composite (collection of components)

Component: Common interface for both leaf and composite objects.

Leaf: Represents simple elements with no children.

Composite: Represents complex elements that can contain children (other components).

  • Treats individual and composite objects uniformly
  • Simplifies code that works with tree-like structures
  • Makes adding new types of components easier

Cons

  • Can make the system overly general
  • Might make it harder to restrict certain behaviors (e.g., preventing a leaf from having children)
java
// Component
interface FileSystemItem {
    void display(String indent);
}

// Leaf
class File implements FileSystemItem {
    private String name;

    public File(String name) {
        this.name = name;
    }

    public void display() {
        System.out.println("File: " + name);
    }
}

// Composite
import java.util.ArrayList;
import java.util.List;

class Folder implements FileSystemItem {
    private String name;
    private List<FileSystemItem> children = new ArrayList<>();

    public Folder(String name) {
        this.name = name;
    }

    public void add(FileSystemItem item) {
        children.add(item);
    }

    public void display() {
        System.out.println("Folder: " + name);
        for (FileSystemItem item : children) {
            item.display();
        }
    }
}

// Main class
public class Main {
    public static void main(String[] args) {
        Folder root = new Folder("root");
        root.add(new File("file1.txt"));
        root.add(new File("file2.txt"));

        Folder images = new Folder("images");
        images.add(new File("photo1.jpg"));
        images.add(new File("photo2.jpg"));

        root.add(images);
        root.display();
    }
}

Decorator

Allows you to dynamically add new behavior to objects without modifying their original code — by "wrapping" them in decorator classes.

###Key features

  • Add behavior without modifying existing code (Open/Closed Principle).
  • Flexible and reusable composition of behaviors.
  • Can mix and match decorators at runtime.
Component (interface)
├── ConcreteComponent (core object)
└── Decorator (wraps a Component)
     └── ConcreteDecorator (adds behavior)

Component: The common interface.

ConcreteComponent: The original object.

Decorator: An abstract class that wraps a Component.

ConcreteDecorator: Extends functionality by overriding behavior.

Cons

  • Many small classes can make the code complex.
  • Debugging can be trickier with multiple layers of wrapping.
java
// Component
interface Coffee {
    String getDescription();
    double cost();
}

// Concrete Component
class SimpleCoffee implements Coffee {
    public String getDescription() {
        return "Simple Coffee";
    }
    public double cost() {
        return 2.0;
    }
}

// Decorator
abstract class CoffeeDecorator implements Coffee {
    protected Coffee decoratedCoffee;

    public CoffeeDecorator(Coffee coffee) {
        this.decoratedCoffee = coffee;
    }
}

// Concrete Decorators
class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

    public String getDescription() {
        return decoratedCoffee.getDescription() + ", Milk";
    }

    public double cost() {
        return decoratedCoffee.cost() + 0.5;
    }
}

class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }

    public String getDescription() {
        return decoratedCoffee.getDescription() + ", Sugar";
    }

    public double cost() {
        return decoratedCoffee.cost() + 0.2;
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        Coffee coffee = new SimpleCoffee();
        coffee = new MilkDecorator(coffee);
        coffee = new SugarDecorator(coffee);

        System.out.println(coffee.getDescription()); // Simple Coffee, Milk, Sugar
        System.out.println("Total cost: $" + coffee.cost()); // 2.7
    }
}

Contact: M_Bergmann AT gmx.at