Structural Design Patterns in Software Engineering

Facade Pattern

The Facade pattern provides a simplified interface to a complex subsystem. It encapsulates interactions with multiple components behind a single entry point, reducing coupling and improving usability.

Use cases:

  • When a subsystem becomes too complex for direct client interaction
  • In layered architectures where each layer exposes a facade to the one above

Benefits:

  • Hides internal complexity
  • Reduces dependencies between subsystems
  • Supports the Law of Demeter (principle of least knowledge)

Drawbacks:

  • May violate the Open/Closed Principle when modifying subsystem behavior
  • Risk of becoming a god object if overused

Example: A points redemption service that internally coordinates inventory validation, points deduction, and logistics—all exposed through a single redeemReward() method.

Adapter Pattern

The Adapter pattern converts the interface of a class into another interface clients expect, enabling incomaptible classes to work together.

Use cases:

  • Integrating legacy or third-party components with mismatched interfaces
  • During system maintenance rather than initial design

Benefits:

  • Promotes reuse without modifying existing code
  • Decouples target and adaptee implementations
  • Adheres to the Open/Closed Principle

Drawbacks:

  • Increases system complexity
  • Can reduce code readability

Class Adapter (via Inheritance)

interface Target {
    void request();
}

class Adaptee {
    public void specificRequest() {
        System.out.println("Adaptee operation");
    }
}

class ClassAdapter extends Adaptee implements Target {
    @Override
    public void request() {
        specificRequest(); // Delegates to adaptee
    }
}

Object Adapter (via Composition)

class ObjectAdapter implements Target {
    private final Adaptee adaptee = new Adaptee();

    @Override
    public void request() {
        adaptee.specificRequest();
    }
}

Bridge Pattern

The Bridge pattern decouples an abstraction from its implementation so both can vary independently. It favors composition over inheritance to avoid class explosion.

Use cases:

  • When both abstraction and implementation have multiple independent dimansions of variation
  • To avoid permanent binding between interface and implementation

Benefits:

  • Enables independent extension of abstraction and implementation
  • Improves extensibility and maintainability
interface Account {
    Account create();
    void displayType();
}

class SavingsAccount implements Account {
    @Override
    public Account create() {
        System.out.println("Savings account opened");
        return this;
    }

    @Override
    public void displayType() {
        System.out.println("Savings Account");
    }
}

abstract class Bank {
    protected Account account;
    public Bank(Account acc) { this.account = acc; }
    abstract Account open();
}

class ICBC extends Bank {
    public ICBC(Account acc) { super(acc); }
    @Override
    Account open() {
        System.out.print("ICBC: ");
        return account.create();
    }
}

// Usage
Bank bank = new ICBC(new SavingsAccount());
Account acc = bank.open();
acc.displayType();

Composite Pattern

The Composite pattern composes objects into tree structures to represent part-whole hierarchies, allowing clients to treat individual and composite objects uniformly.

Use cases:

  • Representing hierarchical structures like file systems or UI components
  • When clients should ignore differences between leaf and composite objects
abstract class Component {
    public void add(Component c) { throw new UnsupportedOperationException(); }
    public void remove(Component c) { throw new UnsupportedOperationException(); }
    public abstract void print();
}

class Leaf extends Component {
    private final String name;
    private final double price;
    public Leaf(String name, double price) {
        this.name = name;
        this.price = price;
    }
    @Override
    public void print() {
        System.out.println("Course: " + name + ", Price: " + price);
    }
}

class Composite extends Component {
    private final List<Component> children = new ArrayList<>();
    private final String title;
    private final int depth;

    public Composite(String title, int depth) {
        this.title = title;
        this.depth = depth;
    }

    @Override
    public void add(Component c) { children.add(c); }

    @Override
    public void print() {
        System.out.println(title);
        for (Component child : children) {
            for (int i = 0; i < depth; i++) System.out.print("  ");
            child.print();
        }
    }
}

// Usage
Component javaCat = new Composite("Java Courses", 2);
javaCat.add(new Leaf("Intro to Java", 55));
javaCat.add(new Leaf("JavaWeb Basics", 66));

Component root = new Composite("Course Catalog", 1);
root.add(javaCat);
root.print();

Decorator Pattern

The Decorator pattern dynamically adds responsibilities to an object without subclassing. It provides a flexible alternative to inheritance for extending functionality.

Use cases:

  • Adding features at runtime
  • When combinations of extensions are needed

Drawbacks:

  • Can lead to many small classes
  • Nested decorators may complicate debugging
// Example: Building a pancake with toppings
Batter base = new PlainPancake();
base = new EggTopping(base);
base = new EggTopping(base); // Double egg
base = new SausageTopping(base);
System.out.println(base.getDescription() + " | Price: $" + base.cost());

Flyweight Pattern

The Flyweight pattern minimizes memory usage by sharing as much data as possible with similar objects. It distinguishes between intrinsic (shared) and extrinsic (unique) state.

Use cases:

  • Applications with large numbers of fine-grained objects (e.g., text editors, game entities)
  • When object creation is expensive
interface Employee {
    void report();
}

class DepartmentManager implements Employee {
    private final String title = "Manager"; // Intrinsic
    private final String dept;              // Intrinsic
    private String content;                 // Extrinsic

    public DepartmentManager(String department) {
        this.dept = department;
    }

    public void setContent(String report) {
        this.content = report;
    }

    @Override
    public void report() {
        System.out.println(content);
    }
}

class EmployeePool {
    private static final Map<String, Employee> cache = new HashMap<>();

    public static Employee getManager(String dept) {
        return cache.computeIfAbsent(dept, d -> {
            DepartmentManager mgr = new DepartmentManager(d);
            mgr.setContent(d + " department report: ...");
            return mgr;
        });
    }
}

// Usage
String[] depts = {"Sales", "R&D", "HR"};
for (int i = 0; i < 10; i++) {
    String d = depts[new Random().nextInt(depts.length)];
    EmployeePool.getManager(d).report();
}

Proxy Pattern

The Proxy pattern provides a surrogate or placeholder for another object to control access to it.

Types:

  • Static proxy: Hand-coded wrapper
  • Dynamic proxy (JDK): Uses java.lang.reflect.InvocationHandler
  • CGLIB proxy: Generates subclasses at runtime for non-interface types
interface GiftService {
    void giveDoll();
}

class Suitor implements GiftService {
    private final String recipient;
    public Suitor(String name) { this.recipient = name; }
    @Override
    public void giveDoll() {
        System.out.println(recipient + " receives a doll");
    }
}

class GiftProxy implements GiftService {
    private final Suitor suitor;
    public GiftProxy(String name) {
        this.suitor = new Suitor(name);
    }
    @Override
    public void giveDoll() {
        System.out.print("[Proxy] Delivering to ");
        suitor.giveDoll();
        System.out.println("... followed by a movie date");
    }
}

// Usage
GiftService proxy = new GiftProxy("Alice");
proxy.giveDoll();

Tags: design-patterns facade adapter Bridge composite

Posted on Thu, 14 May 2026 10:49:07 +0000 by neverett