Mr.Raindrop

Mr.Raindrop

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

Introduction to Design Patterns -- Structural Patterns

Structural patterns are designed to solve how to assemble existing classes and design their interactions 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 issue of object creation, the composition of objects and the dependencies between objects become the focus of developers, as the design of object structures, inheritance, and dependencies 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, thus solving the collaboration problem between classes that cannot work together due to incompatible interfaces. It is similar to a "power converter" in reality, allowing mismatched components to work together through an intermediary 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 whose interface is incompatible with the target.
  • Adapter: The intermediary layer that connects the target and the adaptee, achieving interface conversion through inheritance or composition.
Implementation#
  • Class Adapter (Inheritance Method): The adapter inherits from the adaptee class and implements the target interface. Less flexible

    // 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 Method): 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 uses an abstract class as an intermediary layer, providing default implementations (usually empty or simple logic) for interface methods, allowing subclasses to override specific methods as needed, thus avoiding the redundancy of forcing the implementation of all interface methods.

    // 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 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 would need at least 4 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 versions of each shape in various colors.
  • 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 Instance - 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 both consistently. 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 also known as the Whole-Part Pattern, aims to represent single objects (leaf nodes) and composite objects (branch nodes) using the same interface, ensuring consistency in client usage.

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 do not originally have Add(), Remove(), or GetChild() methods, must implement them (either as empty implementations or throw exceptions), which can lead to security issues.

Implementation of Transparent Mode#

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 Safe Mode#

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

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. 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 of 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: Handling 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 a real subject object
        Subject realSubject = new RealSubject();
        
        // Create a 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.
	In the method body, the actual method of the target object is called using method.invoke(service, args); and the result is stored 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 +  ", logged!");
        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 Usage Example - CSDN Blog

The Decorator Pattern dynamically adds additional responsibilities to an object. In terms of enhancing functionality, the Decorator Pattern is more flexible than generating subclasses. The Decorator Pattern is a structural pattern that acts 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 both the decorated object and the decorator. It defines an object interface that allows these objects to dynamically add responsibilities.

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

  • Decorator (Abstract Decorator): Holds a reference to a Component object and defines a method with the same interface as 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
     */
    @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.