Mr.Raindrop

Mr.Raindrop

一切都在无可挽回的走向庸俗。
twitter
github

Introduction to Design Patterns -- Structural Patterns

The structural pattern is designed to solve how to assemble existing classes and design their interaction methods to achieve certain functional purposes. Structural patterns encompass solutions to many problems, such as extensibility (Facade, Composite, Proxy, Decorator) and encapsulation (Adapter, Bridge).

After addressing the object creation problem, the composition of objects and the dependencies between them become the focus of developers, as how to design the structure, inheritance, and dependencies of objects will affect the maintainability, robustness, and coupling of subsequent programs.

Adapter Pattern#

The Adapter Pattern is a structural design pattern whose core function is to convert the interface of one class into another interface that the client expects, thereby solving the collaboration problem between classes that cannot work together due to incompatible interfaces. It is similar to a "power converter" in reality, allowing originally mismatched components to work together through an intermediate layer interface.

Main Components#
  • Target Interface: The interface expected by the client, defining the methods that need to be called.
  • Adaptee: The interface that needs to be adapted, an existing class or interface, but its interface is incompatible with the target.
  • Adapter: The intermediate layer that connects the target and the adaptee, achieving interface conversion through inheritance or composition.
Implementation#
  • Class Adapter (Inheritance): The adapter inherits the adaptee class and implements the target interface. Less flexibility

    // Target Interface
    interface Target { void request(); }
    // Adaptee
    class Adaptee { void specificRequest() { /*...*/ } }
    // Adapter
    class Adapter extends Adaptee implements Target {
        @Override void request() { specificRequest(); }
    }
    
  • Object Adapter (Composition): The adapter holds an instance of the adaptee object and calls methods through delegation.

    class Adapter implements Target {
        private Adaptee adaptee;
        public Adapter(Adaptee adaptee) { this.adaptee = adaptee; }
        @Override void request() { adaptee.specificRequest(); }
    }
    
  • Interface Adapter (Default Adapter): Solves the problem of too many methods in the target interface while the client only needs partial implementations. It provides default implementations (usually empty or simple logic) for interface methods through an abstract class as an intermediate layer, allowing subclasses to override specific methods as needed, thus avoiding the redundancy of forcing all interface methods to be implemented.

    // Target Interface
    public interface MultiFunction {
        void print();
        void scan();
        void copy();
    }
    
    // Abstract Adapter Class
    public abstract class DefaultAdapter implements MultiFunction {
        @Override
        public void print() {}  // Default empty implementation
        @Override
        public void scan() {}
        @Override
        public void copy() {}
    }
    
    // Concrete Adapter Class
    public class PrinterAdapter extends DefaultAdapter {
        @Override
        public void print() {
            System.out.println("Print function has been enabled");
        }
        // scan() and copy() do not need to be overridden, using default empty logic directly
    }
    
    // Invocation
    public class Client {
        public static void main(String[] args) {
            MultiFunction device = new PrinterAdapter();
            device.print();  // Output: Print function has been enabled
        }
    }
    

Facade Pattern#

The Facade Pattern is a structural design pattern whose core goal is to provide a unified, simplified interface for complex subsystems, thereby hiding the internal complexity of the system, reducing the coupling between the client and the subsystem, and enhancing the usability and maintainability of the system.

Main Components#

Encapsulates the interaction logic of multiple subsystems through a Facade class, providing a high-level interface. The client only needs to interact with the facade class without directly calling complex subsystem components.

  • Facade Role: Unifies and coordinates the calls to the subsystems, providing a simplified interface.
  • Subsystem Role: Modules that implement specific functionalities.
  • Client,

image

Bridge Pattern#

2. Bridge Pattern — Graphic Design Patterns

Imagine if we want to draw rectangles, circles, ellipses, and squares, we need at least four shape classes. However, if the shapes need to have different colors, such as red, green, blue, etc., there are at least two design options:

  • The first design option is to provide a set of various color versions for each shape.
  • The second design option is to combine shapes and colors as needed.

The Bridge Pattern transforms inheritance relationships into association relationships, thereby reducing coupling between classes and minimizing code writing. It separates the abstraction from its implementation, allowing both to vary independently.

Structure of the Bridge Pattern#

Bridge Design Pattern

image

Implementation Example#

Bridge Pattern (Most Understandable Example) - Bridge Pattern Example - CSDN Blog

Composite Pattern#

Understand this tree as a large container that contains many member objects, which can be either container objects or leaf objects. However, due to the functional differences between container objects and leaf objects, we must distinguish between them during use, which can cause unnecessary trouble for clients who wish to treat container objects and leaf objects uniformly. This is the design motivation for the Composite Pattern: The Composite Pattern defines how to recursively combine container objects and leaf objects so that clients can treat them uniformly without distinction.

The Composite Pattern (Composite Pattern) is also known as the Whole-Part Pattern, and its purpose is to represent single objects (leaf nodes) and composite objects (branch nodes) using the same interface, ensuring consistency in the use of single and composite objects by clients.

Structure of the Composite Pattern#

image
The Composite Pattern can be divided into transparent and safe composite patterns. In this approach, since the abstract component declares all methods in all subclasses, the client does not need to distinguish between leaf objects and branch objects, making it transparent to the client. However, its drawback is that leaf components, which originally do not have Add(), Remove(), and GetChild() methods, must implement them (either empty implementations or throw exceptions), which can lead to some safety issues.

Implementation of the Transparent Pattern#

27 Design Patterns - Composite Pattern (Detailed Version) - Zhihu

public class CompositePattern {
    public static void main(String[] args) {
        Component c0 = new Composite();
        Component c1 = new Composite();
        Component leaf1 = new Leaf("1");
        Component leaf2 = new Leaf("2");
        Component leaf3 = new Leaf("3");
        c0.add(leaf1);
        c0.add(c1);
        c1.add(leaf2);
        c1.add(leaf3);
        c0.operation();
    }
}
// Abstract Component
interface Component {
    public void add(Component c);
    public void remove(Component c);
    public Component getChild(int i);
    public void operation();
}
// Leaf Component
class Leaf implements Component {
    private String name;
    public Leaf(String name) {
        this.name = name;
    }
    public void add(Component c) {
    }
    public void remove(Component c) {
    }
    public Component getChild(int i) {
        return null;
    }
    public void operation() {
        System.out.println("Leaf " + name + ": has been accessed!");
    }
}
// Branch Component
class Composite implements Component {
    private ArrayList<Component> children = new ArrayList<Component>();
    public void add(Component c) {
        children.add(c);
    }
    public void remove(Component c) {
        children.remove(c);
    }
    public Component getChild(int i) {
        return children.get(i);
    }
    public void operation() {
        for (Object obj : children) {
            ((Component) obj).operation();
        }
    }
}
Implementation of the Safe Pattern#

Since leaves and branches have different interfaces, the client must be aware of the existence of leaf objects and branch objects when calling, thus losing transparency.
GIF

The Leaf class does not need to implement methods that are useless to it, such as add, remove, and getChild, making the design more reasonable and easier to understand. At the same time, this design also avoids potential errors.

Proxy Pattern#

Controls access to an object by introducing a proxy object to manage that access. The Proxy Pattern allows additional functionality or control over access methods to be added without changing the original object's interface.

Main Components#
  1. Subject (Target Interface):
    • Defines a common interface for both the proxy class and the real subject class.
    • This allows the client to use either the proxy or the real subject object without changing the code.
  2. Real Subject (Real Subject):
    • Implements the Subject interface with specific business logic.
    • This is the object that the client actually needs to access.
  3. Proxy:
    • Also implements the Subject interface and maintains a reference to the Real Subject.
    • The proxy class is responsible for coordinating between the client and the real subject, possibly performing some additional operations before and after the request.

Structure of Proxy Pattern

Static Proxy#

A proxy can only serve one specific business implementation class.

1. Define the Target Interface

First, define an interface that declares the methods to be proxied.

JAVApublic interface Subject {
    void request();
}

2. Implement the Real Subject Class

Create a concrete class that implements the above interface, containing the actual business logic.

JAVApublic class RealSubject implements Subject {
    @Override
    public void request() {
        System.out.println("RealSubject: Processing request");
    }
}

3. Create the Proxy Class

The proxy class also implements the same interface and holds a reference to the real subject object. The proxy class can add additional functionality before or after calling the real subject's methods.

JAVApublic class Proxy implements Subject {
    private RealSubject realSubject;

    public Proxy(RealSubject realSubject) {
        this.realSubject = realSubject;
    }

    @Override
    public void request() {
        // Execute pre-processing operations
        System.out.println("Proxy: Executing pre-processing operations");
        realSubject.request();
        // Execute post-processing operations
        System.out.println("Proxy: Executing post-processing operations");
    }
}

4. Write Client Code

The client interacts with the proxy class or the real subject object through the interface, without needing to know whether it is a direct call or through a proxy.

JAVApublic class Client {
    public static void main(String[] args) {
        // Create the real subject object
        Subject realSubject = new RealSubject();
        
        // Create the proxy object and pass the real subject to the proxy
        Subject proxy = new Proxy(realSubject);
        
        // Call the proxy's method
        proxy.request();
    }
}
Dynamic Proxy#

Dynamic proxies use reflection to create an instance of an interface class at runtime.

// Business Interface
interface DateService {
    void add();
    void del();
}
// Concrete Business Class
class DateServiceImplA implements DateService {
    @Override
    public void add() {
        System.out.println("Successfully added!");
    }

    @Override
    public void del() {
        System.out.println("Successfully deleted!");
    }
}
// Proxy
class ProxyInvocationHandler implements InvocationHandler {
    private DateService service;

    public ProxyInvocationHandler(DateService service) {
        this.service = service;
    }
    
    // this.getClass().getClassLoader() The first is the class loader used to load the proxy class.
    // service.getClass().getInterfaces() All interfaces implemented by the proxied class, allowing the proxy object to provide the same interface view as the target object.
    // this The current instance (i.e., the object implementing the InvocationHandler interface) is used to handle method calls on the proxy object.
    public Object getDateServiceProxy() {
        return Proxy.newProxyInstance(this.getClass().getClassLoader(), service.getClass().getInterfaces(), this);
    }

    /* invoke method:
    The overridden invoke method is triggered whenever any method is called on the proxy object. It takes three parameters:
    Object proxy: Represents the proxy object itself.
    Method method: Represents the target method being called.
    Object[] args: The parameter list passed to the target method.
    Inside the method body, the actual method of the target object is called using method.invoke(service, args); and the result is saved in the result variable.
    Then a log message is printed indicating that the proxy object executed a certain method, recording the method name and return value.
    Finally, the result of the target method's execution is returned.
    */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        var result = method.invoke(service, args); // Let service call the method, method return value
        System.out.println(proxy.getClass().getName() + " proxy class executed " + method.getName() + " method, returned " + result +  ", logging!");
        return result;
    }
}

// Client
public class Test {
    public static void main(String[] args) {
        DateService serviceA = new DateServiceImplA();
        DateService serviceProxy = (DateService) new ProxyInvocationHandler(serviceA).getDateServiceProxy();
        serviceProxy.add();
        serviceProxy.del();
    }
}

Decorator Pattern#

Decorator Pattern - (Most Understandable Example) - Decorator Pattern Example - CSDN Blog

The Decorator Pattern dynamically adds additional responsibilities to an object. In terms of adding functionality, the Decorator Pattern is more flexible than generating subclasses. The Decorator Pattern belongs to structural patterns and serves as a wrapper for existing classes.

Decorator Class Diagram

The main roles of the Decorator Pattern include:

  • Component (Abstract Component): Defines a common interface for the decorated object and the decorator. It defines an object interface that can dynamically add responsibilities to these objects.

  • ConcreteComponent (Concrete Component): Implements the Component interface and is the object being decorated. It defines a specific object that can also have some responsibilities added to it.

  • Decorator (Abstract Decorator): Holds a reference to a Component object and defines a method with the same interface as the Component to maintain consistency.

  • ConcreteDecorator (Concrete Decorator): Implements the methods defined by the Decorator, allowing additional operations to be executed before and after calling the decorated object's methods.

Specific Implementation#
// Abstract Component
public interface INoodles {
    public void cook();
}

// Concrete Component
public class Noodles implements INoodles {

    @Override
    public void cook() {
        System.out.println("Noodles");
    }
}

// Abstract Decorator Class
public abstract class NoodlesDecorator implements INoodles {

    private INoodles noodles;    // Add a reference to INoodles

    public NoodlesDecorator(INoodles noodles){     // Set INoodles through the constructor
        this.noodles = noodles;
    }

    @Override
    public void cook() {
        if (noodles != null){
            noodles.cook();
        }
    }

}

// Concrete Decorator Class
public class EggDecorator extends NoodlesDecorator {

    public EggDecorator(INoodles noodles) {
        super(noodles);
    }

    /**
     * Override the parent's cook method and add its own implementation, calling the parent's cook method,
     * which is the cook operation of noodles passed through this class's constructor
     * EggDecorator(INoodles noodles)
     */
    @Override
    public void cook() {
        System.out.println("Added a poached egg");
        super.cook();
    }
}

// Concrete Decorator Class
public class BeefDecorator extends NoodlesDecorator {

    public BeefDecorator(INoodles noodles) {
        super(noodles);
    }

    @Override
    public void cook() {
        System.out.println("Added a pound of beef");
        super.cook();
    }
}

public class Client {

    public static void main(String[] args) {

        INoodles noodles1 = new Noodles();
        INoodles noodles1WithEgg = new EggDecorator(noodles1);
        noodles1WithEgg.cook();
    }
}
Differences Between Decorator Pattern and Proxy Pattern#
  1. Different Intentions: The Decorator Pattern focuses on enhancing the functionality of an object, while the Proxy Pattern emphasizes controlling access to an object.
  2. Implementation Details: In the Decorator Pattern, both the decorator and the decorated object must implement the same interface to ensure that objects in the decoration chain can be replaced with each other. In the Proxy Pattern, the proxy class and the target object often implement the same interface, but this is mainly to maintain consistency and is not a mandatory condition.
  3. Design Considerations: When using the Decorator Pattern, designers consider how to extend functionality without modifying existing code; when using the Proxy Pattern, they focus more on how to protect and manage access to resources.
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.