Behavioral Design Patterns of Design Patterns

1 Introduction

        Behavioral design patterns are a class of design patterns that focus on communication and interaction between objects, and they are used to solve the problem of responsibility assignment and collaboration between objects. These patterns describe how objects collaborate to accomplish a single task, and how to keep objects loosely coupled.

        Behavioral design patterns can be divided into the following categories:

        1. Template Method Pattern : Define an algorithm framework that allows subclasses to provide implementations for one or more steps.

        2. Observer Pattern (Observer Pattern) : Define one-to-many dependencies between objects, so that when the state of an object changes, all objects that depend on it will be notified and automatically updated.

        3. Strategy Pattern (Strategy Pattern) : Define a series of algorithms, encapsulate them into independent classes, and make them replace each other, so that the changes of the algorithms are independent of the clients who use them.

        4. Command Pattern : Encapsulate requests into objects, allowing different requests, queues, or logs to be used to parameterize other objects. Command mode also supports undoable operations.

        5. State Pattern (State Pattern) : Allows an object to change its behavior when its internal state changes. The object appears to have modified its class.

        6. Chain of Responsibility Pattern : Organize request processors through a chain structure, so that each request can be processed multiple times until a processor handles it.

        7. Interpreter Pattern : Define a language, parse a representation of the language, and perform the operations represented.

        8. Visitor Pattern (Visitor Pattern) : Define a new operation and apply it to a set of objects. The visitor pattern can make operations independent of the object's class.

2. Template method pattern

        The template method pattern is a behavioral design pattern that defines the framework of an algorithm and defers the implementation of some steps to subclasses, so that different subclasses can implement these steps according to their actual needs.

        In the template method pattern, an abstract class is defined, which contains one or more abstract methods, which can be implemented by subclasses, and also contains a concrete template method, which calls the abstract method to complete a specific algorithm flow.

        Here is a simple sample code:

abstract class AbstractClass {
    public void templateMethod() {
        // step 1
        operation1();
        // step 2
        operation2();
        // step 3
        operation3();
    }

    protected abstract void operation1();
    protected abstract void operation2();

    protected void operation3() {
        // 默认实现,子类可以选择重写
    }
}

class ConcreteClass1 extends AbstractClass {
    @Override
    protected void operation1() {
        System.out.println("ConcreteClass1 operation1");
    }

    @Override
    protected void operation2() {
        System.out.println("ConcreteClass1 operation2");
    }
}

class ConcreteClass2 extends AbstractClass {
    @Override
    protected void operation1() {
        System.out.println("ConcreteClass2 operation1");
    }

    @Override
    protected void operation2() {
        System.out.println("ConcreteClass2 operation2");
    }

    @Override
    protected void operation3() {
        System.out.println("ConcreteClass2 operation3");
    }
}

        In this sample code, AbstractClassit is an abstract class, which defines a template method templateMethod(), which contains three steps: operation1(), operation2(), operation3(). Where operation1()and operation2()are abstract methods that need to be implemented by subclasses, but operation3()is a default implementation method, and subclasses can choose to override or not.

  ConcreteClass1and are two concrete subclasses ConcreteClass2respectively , which inherit from AbstractClassand implement the abstract methods in it. The methodConcreteClass2 is overridden , overriding the default implementation.operation3()

        The template method pattern is often used in framework design. The framework defines the framework of the algorithm, and the specific implementation is done by subclasses. For example, in the Servlet specification in Java, a doGet()method , which includes the entire process of HTTP request processing, and the specific implementation is completed by the Servlet subclass.

        Another example of using the template method pattern is the window class in the GUI library. The window class defines the framework for opening, closing, rendering and other operations, and the specific implementation is done by subclasses.

        In short, the template method pattern can help us implement complex algorithms, and can ensure the consistency and scalability of the algorithm during the implementation process.

3. Observer pattern

        The Observer Pattern (Observer Pattern) is a behavioral design pattern, which defines a one-to-many dependency relationship. When the state of the observed object changes, all its dependents will be notified and automatically updated.

The observer pattern consists of the following roles:

  • Subject: The observed object defines the interface for adding, deleting, and notifying observers.
  • Observer: Observer defines an interface for receiving notifications and updating status.
  • ConcreteSubject: The specific observed object maintains its own observer list and is responsible for notifying the observer of state changes.
  • ConcreteObserver: The specific observer maintains a reference to the specific observed object and implements its own update method.

Here is a sample code for a simple observer pattern:

import java.util.ArrayList;
import java.util.List;

interface Observer {
    void update(String message);
}

interface Subject {
    void attach(Observer observer);
    void detach(Observer observer);
    void notifyObservers(String message);
}

class ConcreteSubject implements Subject {
    private List<Observer> observers = new ArrayList<>();

    @Override
    public void attach(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void detach(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers(String message) {
        for (Observer observer : observers) {
            observer.update(message);
        }
    }
}

class ConcreteObserver implements Observer {
    private String name;

    public ConcreteObserver(String name) {
        this.name = name;
    }

    @Override
    public void update(String message) {
        System.out.println(name + " received message: " + message);
    }
}

public class ObserverDemo {
    public static void main(String[] args) {
        Subject subject = new ConcreteSubject();
        Observer observer1 = new ConcreteObserver("Observer1");
        Observer observer2 = new ConcreteObserver("Observer2");

        subject.attach(observer1);
        subject.attach(observer2);

        subject.notifyObservers("Hello World!");

        subject.detach(observer2);

        subject.notifyObservers("Goodbye World!");
    }
}

        In this example, the ConcreteSubject acts as the Observer and the ConcreteObserver acts as the Observer. When the ConcreteSubject's state changes, it notifies all observers and calls their update method.

In Java, the observer pattern also has many application scenarios, the following are some examples:

        1. Java's own event monitoring mechanism

Both the Java AWT and Swing component libraries use the observer pattern to implement the event listener mechanism. When an event occurs, Java invokes the method of the listener registered on the event source.

        2. The event listener mechanism of the Spring framework

In the Spring framework, a flexible event monitoring mechanism is implemented by using the ApplicationEvent and ApplicationListener interfaces. When an event occurs in the application, the Spring container notifies all registered listeners.

        3.Guava EventBus

        An event bus framework is provided in the Google Guava library, which allows developers to more easily implement event-driven programming. The event bus framework is based on the observer pattern, using technologies such as reflection and dynamic proxy, and realizes an efficient event distribution mechanism.

3. Strategy mode

        The strategy pattern is a behavioral design pattern that defines a series of algorithms and encapsulates each algorithm so that they can be replaced with each other. This allows the algorithms to change independently of the clients that use them.

The following is a sample code of the Strategy pattern:

// 策略接口
public interface SortingStrategy {
    public void sort(int[] arr);
}

// 快速排序策略
public class QuickSortStrategy implements SortingStrategy {
    @Override
    public void sort(int[] arr) {
        // 实现快速排序算法
    }
}

// 归并排序策略
public class MergeSortStrategy implements SortingStrategy {
    @Override
    public void sort(int[] arr) {
        // 实现归并排序算法
    }
}

// 策略上下文
public class Sorter {
    private SortingStrategy strategy;

    public void setStrategy(SortingStrategy strategy) {
        this.strategy = strategy;
    }

    public void sort(int[] arr) {
        strategy.sort(arr);
    }
}

// 客户端代码
public static void main(String[] args) {
    int[] arr = {5, 2, 8, 6, 1, 9};

    Sorter sorter = new Sorter();
    sorter.setStrategy(new QuickSortStrategy());
    sorter.sort(arr); // 快速排序

    sorter.setStrategy(new MergeSortStrategy());
    sorter.sort(arr); // 归并排序
}

        In the sample code above, we define an SortingStrategyinterface and two concrete algorithm classes QuickSortStrategyand MergeSortStrategythey both implement SortingStrategythe interface. Then we define a Sorterclass, which holds a SortingStrategymember variable of interface type, which can set different algorithm implementations. Finally, in client code, we can set different algorithms by calling methods Sorteron the object , and then call methods to sort.setStrategy()sort()

        The beauty of the Strategy pattern is that it allows the algorithm to change independently of the client using it. Client code can use a different algorithm without changing its own implementation. In addition, the strategy pattern can also avoid the use of a large number of conditional statements, making the code more concise and readable.

        An example of a practical application of the Strategy pattern is the checkout system of an online shopping site. The settlement system needs to calculate the price of the product according to the payment method selected by the user. Different payment methods may have different preferential policies, so different payment methods can be implemented as different strategy classes, and then use the strategy mode in the settlement system to select the appropriate payment strategy for calculation. This makes it easy to add or modify payment methods without modifying the billing system's code.

4. Chain of Responsibility Model

        The Chain of Responsibility pattern is a behavioral design pattern that allows you to send requests down a chain of handlers until a handler handles the request. Each handler has a reference to the next handler, thus forming a chain.

        In the Chain of Responsibility pattern, a client sends a request to the first processor in the chain, and then each processor in the chain gets a chance to process the request. If the processor is able to handle the request, it processes the request and returns the result; otherwise, it passes the request to the next processor in the chain until the request is processed.

        A simple example of the Chain of Responsibility pattern could be a website's anti-cheat system. When users perform certain activities on the site, such as submitting comments, posting posts, etc., they are verified by the system to ensure that they are not robots. The system usually consists of multiple stages, each with a different verification mechanism. If it fails validation in one stage, the request is passed to the next stage for processing until the request is validated or all stages fail validation.

        Here is a simple Java implementation example:

public abstract class Handler {
    private Handler nextHandler;

    public Handler(Handler nextHandler) {
        this.nextHandler = nextHandler;
    }

    public void handleRequest(Request request) {
        if (canHandleRequest(request)) {
            handle(request);
        } else if (nextHandler != null) {
            nextHandler.handleRequest(request);
        } else {
            System.out.println("No handler found for the request.");
        }
    }

    protected abstract boolean canHandleRequest(Request request);

    protected abstract void handle(Request request);
}

public class Request {
    private String message;

    public Request(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

public class AuthenticationHandler extends Handler {
    public AuthenticationHandler(Handler nextHandler) {
        super(nextHandler);
    }

    @Override
    protected boolean canHandleRequest(Request request) {
        return request.getMessage().startsWith("authenticate:");
    }

    @Override
    protected void handle(Request request) {
        System.out.println("Authenticating the request: " + request.getMessage());
    }
}

public class AuthorizationHandler extends Handler {
    public AuthorizationHandler(Handler nextHandler) {
        super(nextHandler);
    }

    @Override
    protected boolean canHandleRequest(Request request) {
        return request.getMessage().startsWith("authorize:");
    }

    @Override
    protected void handle(Request request) {
        System.out.println("Authorizing the request: " + request.getMessage());
    }
}

public class Main {
    public static void main(String[] args) {
        Handler handlerChain = new AuthenticationHandler(new AuthorizationHandler(null));

        Request request1 = new Request("authenticate:user1");
        handlerChain.handleRequest(request1);

        Request request2 = new Request("authorize:user2");
        handlerChain.handleRequest(request2);

        Request request3 = new Request("invalid_request");
        handlerChain.handleRequest(request3);
    }
}

        The Chain of Responsibility pattern is widely used in many open source frameworks, here are some examples:

  1. The Filter mode in the Java Servlet API, where each Filter can intercept requests and process them. The request is first processed by the first Filter, and then passed to the next Filter in turn until the request is processed.

  2. AOP (Aspect Oriented Programming) in Spring Framework is implemented based on the Chain of Responsibility pattern. Spring AOP allows to separate a cross-cutting concern (such as transaction management, logging, etc.) from the business logic of the application, and weave them into the method call of the target object through a series of enhancers.

  3. Valve mode in Apache Tomcat. Tomcat is an open source web container, which uses a pattern called Valve to implement many functions, such as HTTP request filtering, access control, etc. Each Valve can process the request, and then pass the request to the next Valve until the request is processed.

  4. ChannelPipeline pattern in Netty. Netty is a high-performance network application framework based on NIO, which adopts the chain of responsibility model to realize data processing and transmission. ChannelPipeline in Netty contains a series of ChannelHandlers, each Handler can process data and pass the data to the next Handler.

        In short, the chain of responsibility pattern is a very common design pattern that can help us split the request processing into multiple steps and allow us to dynamically modify the processing flow. This can make the system more flexible and expandable, and improve the maintainability and reusability of the system.

5. Interpreter mode

        Interpreter pattern is a behavioral design pattern used to solve some specific problems. The core idea of ​​this pattern is to define a language grammar and define an interpreter to interpret the statements in the language. Interpreter mode mainly consists of the following two parts:

  1. Abstract Expression (Abstract Expression): defines the interface of the interpreter, which usually contains only one interpret() method.
  2. Concrete Expression: implement the abstract expression interface and explain the expression concretely.

        Here is a simple example of interpreter mode:

// 抽象表达式
interface Expression {
    boolean interpret(String context);
}

// 具体表达式
class TerminalExpression implements Expression {
    private String data;

    public TerminalExpression(String data) {
        this.data = data;
    }

    @Override
    public boolean interpret(String context) {
        if (context.contains(data)) {
            return true;
        }
        return false;
    }
}

// 或表达式
class OrExpression implements Expression {
    private Expression expression1;
    private Expression expression2;

    public OrExpression(Expression expression1, Expression expression2) {
        this.expression1 = expression1;
        this.expression2 = expression2;
    }

    @Override
    public boolean interpret(String context) {
        return expression1.interpret(context) || expression2.interpret(context);
    }
}

// 与表达式
class AndExpression implements Expression {
    private Expression expression1;
    private Expression expression2;

    public AndExpression(Expression expression1, Expression expression2) {
        this.expression1 = expression1;
        this.expression2 = expression2;
    }

    @Override
    public boolean interpret(String context) {
        return expression1.interpret(context) && expression2.interpret(context);
    }
}

// 客户端
public class Client {
    public static void main(String[] args) {
        Expression robert = new TerminalExpression("Robert");
        Expression john = new TerminalExpression("John");
        Expression orExpression = new OrExpression(robert, john);
        Expression julie = new TerminalExpression("Julie");
        Expression married = new TerminalExpression("Married");
        Expression andExpression = new AndExpression(julie, married);
        System.out.println(orExpression.interpret("John"));
        System.out.println(andExpression.interpret("Julie Married"));
    }
}

        The interpreter mode is relatively seldom used in Java, but it is still applied in the parsing of some domain-specific languages, such as parsing in database query languages, regular expression parsing, etc.

        Taking regular expressions as an example, java.util.regexpackages in Java are implemented based on the interpreter mode. In this package, Patterna class represents a regular expression pattern, and Matchera class represents the result of matching a pattern. When matching, Patternthe regular expression is compiled into an interpreter object, and then Matcherthe input string is parsed and matched through the class.

        Specifically, Patternthe class uses the combination mode in the interpreter mode to combine multiple interpreter objects, thereby constructing an interpreter object tree for the entire regular expression. This tree structure includes multiple leaf nodes, and each leaf node represents a basic regular expression syntax element, such as a character or a character class, while non-leaf nodes represent a combination of multiple basic elements, such as one or more characters A repetition of or a union of multiple character classes. MatcherThe class uses the iterator pattern to traverse the interpreter object tree, parsing and matching the input string.

        In short, the application of interpreter mode in Java is relatively specific, and it is generally used in the parsing of domain-specific languages ​​or in specific scenarios such as regular expressions.

Guess you like

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