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