Observer pattern of Java design pattern

Observer mode

Introduction

Remember how the gangsters cooperate to commit a crime in the police and bandit film? When a gang is stealing, there are always one or two people watching at the door - if there is any trouble, they will immediately notify their partners to evacuate urgently. Maybe the people who let the wind do not necessarily know every accomplice in it; And there may be a new little brother who doesn't know the wind. But it doesn't matter. It doesn't affect their communication, because they have a long agreed code.

The relationship between blowers and thieves mentioned above is a living example of the observer model in reality.

Definition and structure

The observer mode is also known as the publish / subscribe mode. GOF defines the observer pattern as follows: it defines a one to many dependency between objects. When the state of an object changes, all objects that depend on it are notified and automatically updated.

Here, let's talk about an important principle of object-oriented design - the principle of single responsibility. Each object of the system should focus on the discrete abstraction in the problem domain. Therefore, ideally, an object does only one thing. This brings many benefits in development: it provides reusability and maintainability, and it is also a good foundation for refactoring.

Therefore, almost all design patterns are based on this basic design principle. I think the origin of observer mode should be in the processing of GUI and business data, because most of the examples explaining observer mode are this subject. However, the application of observer model is by no means limited to this aspect.

Let's look at the components of the observer pattern.

  1. Abstract target role (Subject): the target role knows its observers, and any number of observers can observe the same target. It also provides an interface to register and delete observer objects. Target roles are often implemented by abstract classes or interfaces.

  2. Abstract observer role: define an update interface for objects that need to be notified when the target changes. Abstract observer role is mainly implemented by abstract classes or interfaces.

  3. Concrete subject: store the relevant status into each Concrete Observer object. When its state changes, it notifies its observers.

  4. Concrete Observer role: stores relevant States, which should be consistent with the state of the target. Implement the update interface of Observer to make its own state consistent with the target state. In this role, you can also maintain a reference to the Concrete Subject object.

Put the class diagram of observer pattern, so that the relationship can be clearly expressed.

In the abstract class Subject, the functions mentioned above are provided, and there is a notification method: notify. You can also see that the template mode is used between Subject and ConcreteSubject (this mode is so simple and common that it is used accidentally).

In this way, when the state of a specific target role changes, the notification method will be called according to the Convention. In this method, the corresponding update method will be called one by one according to the list of observers registered in the target role to adjust the state of observers. In this way, the observer mode completes a process.

give an example

The observer pattern is a pattern I left behind in JUnit source code analysis, so JUnit will be used as an example here. JUnit provides users with three different test result display interfaces, and there may be other realistic interfaces in the future. How can the business logic of the test be well separated from the interface displaying the results? Needless to ask, it's observer mode!

Let's take a look at the usage code of observer mode in JUnit

//The following is our abstract observer role. JUnit is implemented by interface, which is also the general way.
//You can see that there are four different update methods defined, corresponding to four different state changes
public interface TestListener {

    /**
     * An error occurred.
     */
    public void addError(Test test, Throwable t);

    /**
     * A failure occurred.
     */
    public void addFailure(Test test, AssertionFailedError t);

    /**
     * A test ended.
     */
    public void endTest(Test test);

    /**
     * A test started.*/
    public void startTest(Test test);
}

//For the specific observer role, we use the simplest TextUI to illustrate it (AWT and swing are very strange to people who do Web applications all day)
public class ResultPrinter implements TestListener {

    //There are many omissions, mainly the display code

    ......

    //The following are the four methods to implement the interface TestListener
    //The behavior of the fill method is simple
    /**
     * @see junit.framework.TestListener#addError(Test, Throwable)
     */
    public void addError(Test test, Throwable t) {
        getWriter().print("E");
    }

    /**
     * @see junit.framework.TestListener#addFailure(Test, AssertionFailedError)
     */
    public void addFailure(Test test, AssertionFailedError t) {
        getWriter().print("F");
    }

    /**
     * @see junit.framework.TestListener#endTest(Test)
     */
    public void endTest(Test test) {
    }

    /**
     * @see junit.framework.TestListener#startTest(Test)
     */
    public void startTest(Test test) {
        getWriter().print(".");
        if (fColumn++ >= 40) {
            getWriter().println();
            fColumn= 0;
        }
    }
}

Let's take a look at our target role. In any case, because JUnit has a simple function and only one target - TestResult, JUnit has only one specific target role.

//What a long code, it seems that there is no point. Most irrelevant information is removed
//The following only lists how to notify the observer when Failures occur
public class TestResult extends Object {
    
    //This is the collection used to store test Failures
    protected Vector fFailures;
    
    //This is the collection used to store registered observers
    protected Vector fListeners;
    
    public TestResult() {
        fFailures= new Vector();
        fListeners= new Vector();
    }
    
    /**
     * Adds a failure to the list of failures. The passed in exception
     * caused the failure.
     */
    public synchronized void addFailure(Test test, AssertionFailedError t) {
        
        fFailures.addElement(new TestFailure(test, t));
        
        //Here is the addFailure method to notify each observer
        for (Enumeration e= cloneListeners().elements(); e.hasMoreElements(); ) {
            ((TestListener)e.nextElement()).addFailure(test, t);
        }
    }
    
    /**
     * Register an observer
     */
    public synchronized void addListener(TestListener listener) {
        fListeners.addElement(listener);
    }
    
    /**
     * Delete an observer
     */
    public synchronized void removeListener(TestListener listener) {
        fListeners.removeElement(listener);
    }
    
    /**
     * Returning a copy of the observer set is, of course, to prevent illegal operations on the observer set
     * You can see that all places that use the observer set pass through it
     */
    private synchronized Vector cloneListeners() {
        return (Vector)fListeners.clone();
    }
    
    ......
}

Well, the roles required for the composition of the observer model are all here. But there seems to be some shortcomings.

There is no real connection between them. In JUnit, it is done through TestRunner, and you can master it flexibly in a specific system.

Take a look at the code in TestRunner:

public class TestRunner extends BaseTestRunner {
    
    private ResultPrinter fPrinter;
    
    public TestResult doRun(Test suite, boolean wait) {
        //Registered here
        result.addListener(fPrinter);
        ......
    }
}

Usage

GOF gives the following cases of using observer mode:

  1. When an abstract model has two aspects, one aspect depends on the other. The two are encapsulated in separate objects so that they can be changed and reused independently.

  2. When the change of one object needs to change other objects at the same time, we don't know how many objects need to be changed.

  3. When an object must notify other objects, it cannot assume who the other objects are. In other words, you don't want these objects to be tightly coupled.

I push you pull

There are two versions of observer mode in the specific implementation of target role and observer role communication. One situation is that after the target role changes, it only tells the observer that the role "I have changed"; If the observer role wants to know the specific change details, it must get them from the interface of the target role. This mode is vividly called pull mode - that is, the changing information is actively "pulled" by the observer role from the target role.

There is another way, that is, my target role is "one-stop service", which informs you of the change and passes the details of the change to the observer role through a parameter. This is the "push mode" - whether you want it or not, here you are.

The use of these two modes depends on the needs of system design. If the target role is complex and the observer role must get some specific change information when updating, the "push mode" is more appropriate. If the target character is relatively simple, the "pull mode" is very appropriate.

Tags: Java Back-end

Posted on Wed, 17 Nov 2021 20:12:54 -0500 by jam123