Structural Design Patterns in Software Engineering
Structural Patterns
Structural patterns describe how classes or objects can be composed to form larger structures. They are categorized into class structural patterns and object structural patterns. The former utilizes inheritance mechanisms to organize interfaces and classes, while the latter employs composition or aggregation to combine objects.
Since composition or aggregation relationships have lower coupling then inheritance, they comply with the 'Composite Reuse Principle', making object structural patterns more flexible than class structural patterns.
The following seven structural patterns exist:
- Proxy Pattern
- Adapter Pattern
- Decorator Pattern
- Bridge Pattern
- Facade Pattern
- Composite Pattern
- Flyweight Pattern
Proxy Pattern
Overview
Sometimes it's necessary to provide a proxy for an object to control access to it. In such cases, direct reference to the target object is not suitable or possible, and the proxy acts as an intermediary between the accessing object and the target object.
In Java, proxies are divided into static and dynamic proxies based on when the proxy class is generated. Static proxies are created at compile time, whereas dynamic proxies are generated at runtime. Dynamic proxies include JDK proxies and CGLib proxies.
Structure
The proxy pattern consists of three roles:
- Subject Interface: Declares business methods implemented by both real subject and proxy objects through an interface or abstract class.
- Real Subject: Implements the specific business of the subject interface, representing the actual object to be referenced.
- Proxy: Provides the same interface as the real subject, containing a reference to it, and can access, control, or extend the real subject's functionality.
Static Proxy
Consider the example of train ticket sales:
If purchasing a train ticket requires going to a station and queuing, it's inconvenient. However, there are ticket agents in various locations where you can buy tickets easily. This exemplifies the proxy pattern, where the station is the target object and the agent is the proxy object.
// Ticket selling interface
public interface TicketSeller {
void sell();
}
// Train station implementing the interface
public class TrainStation implements TicketSeller {
public void sell() {
System.out.println("Selling tickets at train station");
}
}
// Agent point
public class AgentPoint implements TicketSeller {
private TrainStation station = new TrainStation();
public void sell() {
System.out.println("Agent charges service fee");
station.sell();
}
}
// Test class
public class Client {
public static void main(String[] args) {
AgentPoint agent = new AgentPoint();
agent.sell();
}
}
The test class directly accesses the AgentPoint instance, acting as an intermediary between the client and the target object, also enhancing the sell method (charging a service fee).
JDK Dynamic Proxy
Next, we'll implement the above case using dynamic proxy. JDK provides a dynamic proxy class called Proxy, which offers a static method (newProxyInstance) to create proxy objects.
// Ticket selling interface
public interface TicketSeller {
void sell();
}
// Train station
public class TrainStation implements TicketSeller {
public void sell() {
System.out.println("Selling tickets at train station");
}
}
// Factory for creating proxy objects
public class ProxyFactory {
private TrainStation station = new TrainStation();
public TicketSeller getProxyObject() {
TicketSeller seller = (TicketSeller) Proxy.newProxyInstance(
station.getClass().getClassLoader(),
station.getClass().getInterfaces(),
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Agent charges service fee (JDK dynamic proxy)");
Object result = method.invoke(station, args);
return result;
}
});
return seller;
}
}
// Test class
public class Client {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory();
TicketSeller proxy = factory.getProxyObject();
proxy.sell();
}
}
Using dynamic proxy raises questions about:
-
Is
ProxyFactorythe proxy class?ProxyFactoryis not the proxy class in the traditional sense; instead, the proxy class is dynamically generated at runtime. Using tools like Arthas, we can inspect the generated proxy class structure:package com.sun.proxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public final class $Proxy0 extends Proxy implements TicketSeller { private static Method m3; public $Proxy0(InvocationHandler invocationHandler) { super(invocationHandler); } static { try { m3 = Class.forName("com.example.TicketSeller").getMethod("sell", new Class[0]); } catch (Exception e) { throw new RuntimeException(e); } } public final void sell() { try { this.h.invoke(this, m3, null); } catch (Throwable throwable) { throw new RuntimeException(throwable); } } }From this class, we observe:
- The proxy class ($Proxy0) implements
TicketSeller, confirming that the proxy and real object share the same interface. - The proxy class passes our anonymous inner class to its parent.
- The proxy class ($Proxy0) implements
-
What is the execution flow of dynamic proxy?
Below are key excerpts from the implementation:
// Runtime-generated proxy class public final class $Proxy0 extends Proxy implements TicketSeller { private static Method m3; public $Proxy0(InvocationHandler invocationHandler) { super(invocationHandler); } static { m3 = Class.forName("com.example.TicketSeller").getMethod("sell", new Class[0]); } public final void sell() { this.h.invoke(this, m3, null); } } // JDK's dynamic proxy related class public class Proxy implements java.io.Serializable { protected InvocationHandler h; protected Proxy(InvocationHandler h) { this.h = h; } } // Proxy factory public class ProxyFactory { private TrainStation station = new TrainStation(); public TicketSeller getProxyObject() { TicketSeller seller = (TicketSeller) Proxy.newProxyInstance( station.getClass().getClassLoader(), station.getClass().getInterfaces(), new InvocationHandler() { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("Agent charges service fee (JDK dynamic proxy)"); Object result = method.invoke(station, args); return result; } }); return seller; } } // Test class public class Client { public static void main(String[] args) { ProxyFactory factory = new ProxyFactory(); TicketSeller proxy = factory.getProxyObject(); proxy.sell(); } }Execution flow:
- The test class calls the
sell()method on the proxy object. - Due to polymorphism, the
sell()method in the proxy class ($Proxy0) is executed. - The proxy class calls the
invokemethod of theInvocationHandlerimplementation. - The
invokemethod uses reflection to call thesell()method in the real object (TrainStation).
- The test class calls the
CGLIB Dynamic Proxy
For the same example, let's implement it using CGLIB proxy.
If no interface is defined for TrainStation, JDK proxy cannot be used since it requires an interface. CGLIB is a third-party library providing dynamic proxy capabilities for classes without interfaces.
Add the dependency:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
</dependency>
// Train station
public class TrainStation {
public void sell() {
System.out.println("Selling tickets at train station");
}
}
// Proxy factory
public class ProxyFactory implements MethodInterceptor {
private TrainStation target = new TrainStation();
public TrainStation getProxyObject() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(target.getClass());
enhancer.setCallback(this);
TrainStation obj = (TrainStation) enhancer.create();
return obj;
}
public TrainStation intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("Agent charges service fee (CGLIB dynamic proxy)");
TrainStation result = (TrainStation) methodProxy.invokeSuper(o, args);
return result;
}
}
// Test class
public class Client {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory();
TrainStation proxy = factory.getProxyObject();
proxy.sell();
}
}
Comparison Between Proxy Types
-
JDK vs CGLIB Proxies
CGLIB uses ASM bytecode generation framework and generates proxy classes at runtime, offering better performance than JDK proxy in older JDK versions. However, CGLIB cannot proxy final classes or methods.
In JDK 1.6–1.8, JDK proxy has become more efficient than CGLIB under heavy usage.
-
Dynamic vs Static Proxy
Dynamic proxy centralizes method handling in
InvocationHandler.invoke. This reduces maintenance overhead compared to static proxy, which requires each method to be manually delegated.
Advantages and Disadvantages
Advantages:
- Mediates access between client and target object.
- Extends functionality of target object.
- Reduces coupling between system components.
Disadvantages:
- Increases system complexity.
Use Cases
- Remote proxies
- Firewall proxies
- Access control proxies
Adapter Pattern
Overview
When traveling to Europe, local sockets differ from those used in China. A converter is needed to adapt one to the other. Similarly, adapters convert one interface into another to enable incompatible systems to work together.
Definition:
Converts the interface of a class into another interface clients expect, allowing classes that couldn't otherwise work together due to incompatible interfaces to collaborate.
Adapter patterns come in class adapter and object adapter forms. Class adapters are less preferred due to higher coupling and dependency on internal structures.
Structure
- Target Interface: Expected interface by current system.
- Adaptee Class: Existing component interface to be adapted.
- Adapter Class: Transforms adaptee interface into target interface.
Class Adapter Pattern
Implementation involves creating an adapter class that implements the target interface and inherits from the existing component.
Example: Card reader for TF cards.
// SD card interface
public interface SDCard {
String readSD();
void writeSD(String msg);
}
// SD card implementation
public class SDCardImpl implements SDCard {
public String readSD() {
return "sd card read a msg :hello word SD";
}
public void writeSD(String msg) {
System.out.println("sd card write msg : " + msg);
}
}
// Computer class
public class Computer {
public String readSD(SDCard sdCard) {
if(sdCard == null) {
throw new NullPointerException("sd card null");
}
return sdCard.readSD();
}
}
// TF card interface
public interface TFCard {
String readTF();
void writeTF(String msg);
}
// TF card implementation
public class TFCardImpl implements TFCard {
public String readTF() {
return "tf card read msg : hello word tf card";
}
public void writeTF(String msg) {
System.out.println("tf card write a msg : " + msg);
}
}
// Adapter class
public class SDAdapterTF extends TFCardImpl implements SDCard {
public String readSD() {
System.out.println("adapter read tf card ");
return readTF();
}
public void writeSD(String msg) {
System.out.println("adapter write tf card");
writeTF(msg);
}
}
// Test class
public class Client {
public static void main(String[] args) {
Computer computer = new Computer();
SDCard sdCard = new SDCardImpl();
System.out.println(computer.readSD(sdCard));
System.out.println("------------");
SDAdapterTF adapter = new SDAdapterTF();
System.out.println(computer.readSD(adapter));
}
}
This approach violates the composite reuse principle. Itt's best suited when the client already expects an interface.
Object Adapter Pattern
Implementation uses composition instead of inheritance. The adapter holds a reference to the adaptee and implements the target interface.
// Adapter class
public class SDAdapterTF implements SDCard {
private TFCard tfCard;
public SDAdapterTF(TFCard tfCard) {
this.tfCard = tfCard;
}
public String readSD() {
System.out.println("adapter read tf card ");
return tfCard.readTF();
}
public void writeSD(String msg) {
System.out.println("adapter write tf card");
tfCard.writeTF(msg);
}
}
// Test class
public class Client {
public static void main(String[] args) {
Computer computer = new Computer();
SDCard sdCard = new SDCardImpl();
System.out.println(computer.readSD(sdCard));
System.out.println("------------");
TFCard tfCard = new TFCardImpl();
SDAdapterTF adapter = new SDAdapterTF(tfCard);
System.out.println(computer.readSD(adapter));
}
}
Application Scenarios
- Integrating legacy components with new systems.
- Using third-party libraries with incompatible interfaces.
JDK Source Analysis
InputStreamReader in Java uses the adapter pattern to bridge InputStream and Reader.
public int read() throws IOException {
return sd.read();
}
public int read(char cbuf[], int offset, int length) throws IOException {
return sd.read(cbuf, offset, length);
}
From the Sun JDK implementation, the StreamDecoder class encapsulates the conversion between byte streams and character streams.
Decorator Pattern
Overview
Consider a fast-food restaurant scenario where dishes can be enhanced with additionnal ingredients like eggs or bacon. Each ingredient adds a cost, and calculating total prices becomes complex with inheritance.
Definition:
Adds responsibilities to objects dynamically without modifying their structure.
Structure
- Component: Defines the interface for objects.
- Concrete Component: Implements the component interface.
- Decorator: Inherits or implements component and holds a reference to it.
- Concrete Decorator: Adds specific behaviors to the component.
Example
Improve the fast-food example using the decorator pattern.
// Fast food base class
public abstract class FastFood {
private float price;
private String description;
public FastFood(float price, String desc) {
this.price = price;
this.description = desc;
}
public abstract float cost();
public float getPrice() { return price; }
public String getDescription() { return description; }
}
// Concrete fast food items
public class FriedRice extends FastFood {
public FriedRice() {
super(10, "Fried Rice");
}
public float cost() {
return getPrice();
}
}
public class FriedNoodles extends FastFood {
public FriedNoodles() {
super(12, "Fried Noodles");
}
public float cost() {
return getPrice();
}
}
// Garnish base class
public abstract class Garnish extends FastFood {
private FastFood food;
public Garnish(FastFood food, float price, String desc) {
super(price, desc);
this.food = food;
}
public FastFood getFood() { return food; }
}
// Specific garnishes
public class Egg extends Garnish {
public Egg(FastFood food) {
super(food, 1, "Egg");
}
public float cost() {
return getPrice() + getFood().getPrice();
}
public String getDescription() {
return super.getDescription() + " with " + getFood().getDescription();
}
}
public class Bacon extends Garnish {
public Bacon(FastFood food) {
super(food, 2, "Bacon");
}
public float cost() {
return getPrice() + getFood().getPrice();
}
public String getDescription() {
return super.getDescription() + " with " + getFood().getDescription();
}
}
// Test class
public class Client {
public static void main(String[] args) {
FastFood meal = new FriedRice();
System.out.println(meal.getDescription() + " costs " + meal.cost() + " yuan");
FastFood mealWithEgg = new Egg(new FriedRice());
System.out.println(mealWithEgg.getDescription() + " costs " + mealWithEgg.cost() + " yuan");
FastFood mealWithBacon = new Bacon(new FriedNoodles());
System.out.println(mealWithBacon.getDescription() + " costs " + mealWithBacon.cost() + " yuan");
}
}
Benefits
- More flexible than inheritance for adding features.
- Supports dynamic addition of responsibilities.
- Separates decoration logic from core object.
Use Cases
- When inheritance isn't feasible or leads to many subclasses.
- For adding functionalities dynamically.
JDK Source Analysis
IO stream wrappers like BufferedWriter use the decorator pattern.
Bridge Pattern
Overview
When dealing with multiple dimensions that can vary independently, such as shapes and colors, inheritance leads to exponential class proliferation. The bridge pattern separates abstraction from implementation, enabling independent evolution of both.
Definition:
Separates abstraction from implementation so both can vary independently.
Structure
- Abstraction: Defines the abstraction interface and holds a reference to the implementation.
- Refined Abstraction: Extends the abstraction and delegates to the implementation.
- Implementor: Defines the interface for implementation.
- Concrete Implementor: Implements the implementor interface.
Example
Video player across platforms:
// Video file interface
public interface VideoFile {
void decode(String fileName);
}
// Concrete implementations
public class AVIFile implements VideoFile {
public void decode(String fileName) {
System.out.println("Playing avi file: " + fileName);
}
}
public class RMVBFile implements VideoFile {
public void decode(String fileName) {
System.out.println("Playing rmvb file: " + fileName);
}
}
// Operating system base
public abstract class OSVersion {
protected VideoFile videoFile;
public OSVersion(VideoFile videoFile) {
this.videoFile = videoFile;
}
public abstract void play(String fileName);
}
// Concrete operating systems
public class WindowsOS extends OSVersion {
public WindowsOS(VideoFile videoFile) {
super(videoFile);
}
public void play(String fileName) {
videoFile.decode(fileName);
}
}
public class MacOS extends OSVersion {
public MacOS(VideoFile videoFile) {
super(videoFile);
}
public void play(String fileName) {
videoFile.decode(fileName);
}
}
// Test class
public class Client {
public static void main(String[] args) {
OSVersion os = new WindowsOS(new AVIFile());
os.play("Movie Title");
}
}
Benefits
- Independent extension of abstraction and implementation.
- Improved flexibility and maintainability.
Use Cases
- Systems requiring independent evolution of two dimensions.
Facade Pattern
Overview
The facade pattern simplifies access to complex subsystems by providing a unified interface. It's also known as the 'Law of Demeter' application.
Definition:
Provides a single interface to a set of interfaces in a subsystem, making it easier to use.
Structure
- Facade: Offers a simplified interface to the subsystem.
- Subsystems: Individual components within the system.
Example
Smart home appliance control:
// Components
public class Light {
public void turnOn() { System.out.println("Light on"); }
public void turnOff() { System.out.println("Light off"); }
}
public class TV {
public void turnOn() { System.out.println("TV on"); }
public void turnOff() { System.out.println("TV off"); }
}
public class AirConditioner {
public void turnOn() { System.out.println("AC on"); }
public void turnOff() { System.out.println("AC off"); }
}
// Facade
public class SmartHomeFacade {
private Light light = new Light();
private TV tv = new TV();
private AirConditioner ac = new AirConditioner();
public void wakeUp() {
System.out.println("Waking up");
light.turnOn();
tv.turnOn();
ac.turnOn();
}
public void goSleep() {
System.out.println("Going to sleep");
light.turnOff();
tv.turnOff();
ac.turnOff();
}
}
// Test class
public class Client {
public static void main(String[] args) {
SmartHomeFacade facade = new SmartHomeFacade();
facade.wakeUp();
facade.goSleep();
}
}
Benefits
- Simplifies interaction with complex subsystems.
- Reduces coupling between components.
Drawbacks
- Violates open/closed principle.
Use Cases
- Layered system architecture.
- Complex subsystems requiring simple interfaces.
JDK Source Analysis
Tomcat's RequestFacade demonstrates the facade pattern, hiding internal request details from servlets.