Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Structural Design Patterns in Software Engineering

Tech May 7 5

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 ProxyFactory the proxy class?

    ProxyFactory is 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.
  • 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:

    1. The test class calls the sell() method on the proxy object.
    2. Due to polymorphism, the sell() method in the proxy class ($Proxy0) is executed.
    3. The proxy class calls the invoke method of the InvocationHandler implementation.
    4. The invoke method uses reflection to call the sell() method in the real object (TrainStation).

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.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.