Structural Design Patterns of Design Patterns

1. Overview and classification of structural design patterns

        Structural design patterns refer to a set of design patterns for designing relationships between objects and classes. They usually deal with how to organize code for optimal flexibility and maintainability.

Structural design patterns can be divided into the following categories:

  1. Adapter Pattern (Adapter Pattern): Used to convert the interface of a class to another interface that the client wants. The Adapter pattern enables classes that would otherwise not work together due to incompatible interfaces to work together.
  2. Bridge Pattern: It is used to separate the abstract part from the implementation part so that they can change independently without affecting each other.
  3. Composite Pattern: It is used to combine objects into a tree structure to represent the "whole-part" hierarchy. The Composite pattern enables clients to treat individual objects as well as composite objects.
  4. Decorator Pattern: It is used to dynamically add additional responsibilities to objects without changing the object interface. The decorator pattern is more flexible and easier to extend than generating subclasses.
  5. Facade Pattern: Provide a simple interface for complex subsystems for easy use by clients.
  6. Flyweight Pattern: Reduce memory usage and object creation overhead by sharing a large number of fine-grained objects.
  7. Proxy Pattern: It is used to provide a proxy for other objects to control access to this object.

        These patterns can be used in combination, for example we can choose between Adapter pattern and Proxy pattern to dynamically add or remove proxies when needed. Or we can choose between the decorator pattern and the facade pattern to hide some of the complexity when it is not needed.

2. Adapter mode

        The adapter pattern is a structural design pattern that allows converting an interface to another interface expected by clients. It is often used to resolve problems between two incompatible interfaces, or to adapt an old interface to a new one.

        In the adapter mode, the adapter acts as an intermediate layer, passing the client's request to the adapted object, and then passing the adapted object's response back to the client. The adapter is essentially a wrapper that implements the interface required by the client, and converts the client's request into a format that the adapter object can understand.

        There are two common implementations of the adapter pattern: class adapters and object adapters. Class adapters use multiple inheritance to adapt to two different interfaces, while object adapters implement interface conversion by including an instance of the object being adapted.

        The adapter pattern is very extensive in practical applications. For example, a lot of software contains code based on older APIs that are obsolete in newer versions. In this case, the adapter pattern can be used to adapt the old API to the new API to maintain code compatibility. In addition, the adapter mode is often used to adapt different database APIs into a unified interface, so that developers can switch between different databases easily.

        Below is a sample code of the adapter pattern, assuming we have a class from an external library, but its interface does not meet our needs. We can create an adapter class that implements the interface we need and has the object of the external library class as its member variable. The method of the adapter class will call the method of the external library class and convert its result to an interface suitable for us.

// 外部库的类,其接口不符合我们的需求
public class ExternalLibraryClass {
    public void doSomethingComplicated() {
        // ...
    }
}

// 我们需要一个能够调用doSomethingComplicated()方法的接口
public interface ComplicatedInterface {
    void doSomething();
}

// 适配器类,将ExternalLibraryClass适配为ComplicatedInterface
public class Adapter implements ComplicatedInterface {
    private ExternalLibraryClass externalLibrary;

    public Adapter(ExternalLibraryClass externalLibrary) {
        this.externalLibrary = externalLibrary;
    }

    @Override
    public void doSomething() {
        externalLibrary.doSomethingComplicated();
    }
}

// 使用适配器来调用外部库的方法
public class Client {
    public static void main(String[] args) {
        ExternalLibraryClass externalLibrary = new ExternalLibraryClass();
        Adapter adapter = new Adapter(externalLibrary);
        adapter.doSomething();
    }
}

3. Bridge mode

        The Bridge Pattern is a structural design pattern that can split a large class or a series of closely related classes into two separate hierarchies of abstraction and implementation, so that they can change independently in the two levels .

        The core idea of ​​the bridge pattern is to separate abstraction and implementation so that they can vary independently without affecting each other. In the bridge pattern, the abstract part usually defines some operations, and the implementation part realizes these operations. Both parts can be expanded independently without affecting each other.

        The following is a simple sample code of the bridge mode, which implements a Shape interface and a DrawAPI interface, where the Shape interface is the abstract part, and the DrawAPI interface is the implementation part. Concrete graphics classes, such as Circle and Rectangle, inherit the Shape interface, and can call methods in the DrawAPI interface to draw.

interface Shape {
    void draw();
}

class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing Rectangle");
    }
}

class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing Circle");
    }
}

abstract class ShapeColor {
    protected Shape shape;
    
    public ShapeColor(Shape shape) {
        this.shape = shape;
    }
    
    abstract public void drawWithColor(String color);
}

class RedShapeColor extends ShapeColor {
    public RedShapeColor(Shape shape) {
        super(shape);
    }
    
    public void drawWithColor(String color) {
        System.out.println(color + " ");
        shape.draw();
    }
}

public class Main {
    public static void main(String[] args) {
        Shape rectangle = new Rectangle();
        Shape circle = new Circle();
        
        ShapeColor redRectangle = new RedShapeColor(rectangle);
        ShapeColor redCircle = new RedShapeColor(circle);
        
        redRectangle.drawWithColor("Red");
        redCircle.drawWithColor("Red");
    }
}

        In this example, we have an interface Shapeand two implementing classes Rectangleand Circle. Next, we create an abstract class ShapeColorthat holds an Shapeobject and defines an abstract method drawWithColor. We also define an implementing class RedShapeColor, which inherits from ShapeColor, and overrides drawWithColorthe method . In this method, we print the color and Shapecall drawthe method of the object passed in.

        In Mainthe class , we create a Rectangleobject and a Circleobject , and then use these two objects to create a RedShapeColorobject. Finally, we call drawWithColorthe method and pass a color parameter.

        In practice, the bridge pattern can be used to separate abstraction from its implementation so that they can vary independently. A common example is a data access framework used to support many different types of databases (such as Oracle, MySQL, etc.). In this case, the bridge pattern can be used to separate the database access interface (such as JDBC) from the implementation of different types of databases. This allows us to support new database types by changing the implementation without having to change the client code.

4. Combination mode

        The Composite pattern is a structural design pattern that allows you to organize objects into a tree structure to represent a whole-part hierarchy. The Composite pattern simplifies the client code by enabling the client to handle single objects and object combinations uniformly.

        Composite schemas contain two types of objects: composite objects and leaf objects. Composition objects can contain other composition objects and/or leaf objects, and leaf objects are the most basic objects and cannot contain other objects. These objects can form a tree-like structure, where composite objects are non-leaf nodes and leaf objects are end nodes of the tree.

        Here is an example of a simple composite pattern:

// 定义抽象组件类
abstract class Component {
    public abstract function operation();
}

// 定义叶子节点类
class Leaf extends Component {
    public function operation() {
        // 叶子节点操作
    }
}

// 定义组合节点类
class Composite extends Component {
    private $children = array();

    public function add(Component $component) {
        $this->children[] = $component;
    }

    public function remove(Component $component) {
        // 从 $children 数组中删除指定组件
    }

    public function operation() {
        // 组合节点操作
        foreach ($this->children as $child) {
            $child->operation();
        }
    }
}

        In the example above, Componentthe class is the abstract component class in the composite pattern, which defines an operation operation(). LeafThe class is the leaf node class, which implements operation()the method . CompositeThe class is a composite node class, which contains an array $childrenfor storing child nodes, and add()and remove()methods for adding and removing child nodes. CompositeThe class also implements operation()a method that recursively calls operation()the method of all child nodes.

Composite patterns can be applied in many scenarios, such as:

  • Graphical User Interface (GUI) components, such as windows, buttons, and text boxes, that can be nested and combined to create more complex GUI elements.
  • A file system in which directories and files can be organized into a tree structure.
  • Organizational structure, where companies can be organized into a tree structure, including departments and employees etc.

In summary, the composition pattern can simplify the code that deals with object composition and provide a more flexible design to adapt to changing needs.

5. Decorator pattern

        The decorator pattern is a structural design pattern that allows adding behavior to an object dynamically without changing the original object. This pattern is implemented by creating a wrapper object that wraps one or more decorator objects around the original object. In this way, behaviors can be dynamically added, modified, or removed without changing the original object.

In the decorator pattern, there are four core roles:

  1. Abstract Component (Component): Define an object interface that can dynamically add responsibilities or behaviors to these objects.

  2. Concrete Component: implement the abstract component interface, that is, the specific object to be added with behavior.

  3. Abstract decorator (Decorator): Inherit or implement the abstract component interface and contain a reference to the abstract component.

  4. Concrete Decorator: Implement the abstract decorator interface and specifically implement the behavior to be added.

Here is an example of a simple decorator pattern to implement a cup of coffee example:        

// 抽象构件
interface Coffee {
    String getDescription();
    double getCost();
}

// 具体构件
class Espresso implements Coffee {
    @Override
    public String getDescription() {
        return "Espresso";
    }

    @Override
    public double getCost() {
        return 1.99;
    }
}

// 抽象装饰器
abstract class CoffeeDecorator implements Coffee {
    private final Coffee coffee;

    public CoffeeDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public String getDescription() {
        return coffee.getDescription();
    }

    @Override
    public double getCost() {
        return coffee.getCost();
    }
}

// 具体装饰器
class Mocha extends CoffeeDecorator {
    public Mocha(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return super.getDescription() + ", Mocha";
    }

    @Override
    public double getCost() {
        return super.getCost() + 0.50;
    }
}

class Whip extends CoffeeDecorator {
    public Whip(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return super.getDescription() + ", Whip";
    }

    @Override
    public double getCost() {
        return super.getCost() + 0.30;
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        Coffee espresso = new Espresso();
        System.out.println(espresso.getDescription() + " $" + espresso

        Application scenario:

  1. Logging: On the premise of not modifying the original business logic, log some operations in the system to facilitate subsequent troubleshooting and data statistics.

  2. Dynamic proxy: Without changing the original code structure, access is controlled through proxy classes. For example, AOP (aspect-oriented programming) in Spring is implemented through dynamic proxy.

  3. Cache: When certain operations in the system are called frequently, the decorator mode can be used to implement caching to improve system performance.

  4. Access control: access control is implemented through the decorator mode, and access control can be performed on certain operations without modifying the original code structure.

6. Proxy mode

        The proxy pattern is a structural design pattern that allows controlling access to another object by providing a proxy object. The proxy pattern can be used to restrict access to an object, provide additional functionality to an object, or hide the implementation details of an object.

        In the Proxy pattern, there are three main actors: the client, the proxy object, and the real object. The client accesses the real object through the proxy object, and the proxy object is responsible for processing the client request and passing the request to the real object when necessary.

        Here is a simple example of a proxy pattern:

interface Image {
    void display();
}

class RealImage implements Image {
    private String fileName;

    public RealImage(String fileName) {
        this.fileName = fileName;
        loadFromDisk(fileName);
    }

    @Override
    public void display() {
        System.out.println("Displaying " + fileName);
    }

    private void loadFromDisk(String fileName) {
        System.out.println("Loading " + fileName);
    }
}

class ImageProxy implements Image {
    private RealImage realImage;
    private String fileName;

    public ImageProxy(String fileName) {
        this.fileName = fileName;
    }

    @Override
    public void display() {
        if (realImage == null) {
            realImage = new RealImage(fileName);
        }
        realImage.display();
    }
}

public class Main {
    public static void main(String[] args) {
        Image image = new ImageProxy("test.jpg");
        image.display();
    }
}

        In the above example, we defined an Imageinterface and its two implementation classes: RealImageand ImageProxy. RealImagerepresents an actual image, ImageProxyand is RealImagea proxy for . When a client ImageProxyaccesses RealImage, ImageProxyit will first check to see RealImageif it already exists, create it if not, and then call the ' RealImages displaymethod to display the image.

The proxy pattern has many uses in practical applications, some of which include:

  • Remote Proxy: Encapsulates a remote object in a proxy object so that clients can access it as if it were a local object.
  • Virtual Proxy: Defers the overhead of loading an object until it is actually needed.
  • Security Proxy: Controls access to an object to ensure only authorized users can access it.

        In Java, the proxy pattern has been widely used. For example, in the Spring framework, AOP (aspect-oriented programming) is an implementation based on the proxy mode. AOP can intercept method calls and perform specific operations through proxies, such as logging, performance monitoring, security checks, etc.

Guess you like

Origin blog.csdn.net/zz18532164242/article/details/130352995