Structural Design Patterns in Java
Introduction
In software engineering, Structural Design Patterns deal with object composition and the way classes and objects are assembled to form larger, more complex structures. These patterns help ensure that when one part of a system changes, the entire structure does not collapse. They provide mechanisms to build scalable and flexible architectures by focusing on the relationships between entities.
The Gang of Four (GoF) defines seven structural patterns. This guide covers the most prominent ones used in modern Java development.
1. Adapter Pattern
Intent: Converts the interface of a class into another interface the clients expect. It lets classes work together that couldn't otherwise because of incompatible interfaces.
Real-world analogy: A power adapter allowing a European plug to work in a US wall outlet.
Implementation:
// Target interface
interface WebDriver {
void navigate(String url);
}
// Incompatible Adaptee
class LegacyBrowser {
void openPage(String uri) {
System.out.println("Legacy opening: " + uri);
}
}
// Adapter
class BrowserAdapter implements WebDriver {
private LegacyBrowser legacyBrowser;
public BrowserAdapter(LegacyBrowser legacyBrowser) {
this.legacyBrowser = legacyBrowser;
}
@Override
public void navigate(String url) {
legacyBrowser.openPage(url);
}
}
2. Decorator Pattern
Intent: Attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
Real-world analogy: Adding toppings (cheese, pepperoni) to a base pizza.
Implementation: (Java's java.io.InputStream heavily uses this).
interface Coffee {
String getDescription();
double getCost();
}
class SimpleCoffee implements Coffee {
public String getDescription() { return "Simple Coffee"; }
public double getCost() { return 2.0; }
}
// Decorator
abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) { this.decoratedCoffee = coffee; }
public String getDescription() { return decoratedCoffee.getDescription(); }
public double getCost() { return decoratedCoffee.getCost(); }
}
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) { super(coffee); }
public String getDescription() { return super.getDescription() + ", Milk"; }
public double getCost() { return super.getCost() + 0.5; }
}
3. Facade Pattern
Intent: Provides a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.
Real-world analogy: A smart home dashboard where a single button ("Movie Mode") dims lights, turns on the TV, and lowers the blinds.
Implementation:
class VideoEncoder { public void encode() { /* complex logic */ } }
class AudioMixer { public void mix() { /* complex logic */ } }
// Facade
class MediaConverterFacade {
public void convertVideo(String file) {
VideoEncoder encoder = new VideoEncoder();
AudioMixer mixer = new AudioMixer();
mixer.mix();
encoder.encode();
System.out.println("Video converted successfully.");
}
}
4. Composite Pattern
Intent: Composes objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.
Real-world analogy: A file system containing files and folders, where a folder can contain other files and folders.
Implementation:
interface FileSystemComponent {
void showDetails();
}
class File implements FileSystemComponent {
private String name;
public File(String name) { this.name = name; }
public void showDetails() { System.out.println("File: " + name); }
}
class Directory implements FileSystemComponent {
private List<FileSystemComponent> components = new ArrayList<>();
private String name;
public Directory(String name) { this.name = name; }
public void addComponent(FileSystemComponent component) { components.add(component); }
public void showDetails() {
System.out.println("Directory: " + name);
for (FileSystemComponent c : components) {
c.showDetails();
}
}
}
5. Proxy Pattern
Intent: Provides a surrogate or placeholder for another object to control access to it. It can add functionalities like lazy initialization, access control, or logging.
Real-world analogy: A credit card acts as a proxy for your bank account.
Implementation: (Hibernate uses proxies for lazy-loading entities).
interface Image { void display(); }
class RealImage implements Image {
private String filename;
public RealImage(String filename) {
this.filename = filename;
loadFromDisk();
}
private void loadFromDisk() { System.out.println("Loading " + filename); }
public void display() { System.out.println("Displaying " + filename); }
}
class ProxyImage implements Image {
private RealImage realImage;
private String filename;
public ProxyImage(String filename) { this.filename = filename; }
public void display() {
if (realImage == null) {
realImage = new RealImage(filename); // Lazy initialization
}
realImage.display();
}
}
Summary
- Adapter: Bridges incompatible interfaces.
- Decorator: Dynamically adds behavior wrapping objects.
- Facade: Simplifies complex subsystems behind a clean interface.
- Composite: Treats individuals and groups uniformly via trees.
- Proxy: Controls access to an object (lazy loading, security, caching).