[easily understand design mode] observer mode

Posted by holowugz on Thu, 10 Feb 2022 09:46:05 +0100

[design mode] observer mode

preface

The definition of Observer mode: it refers to the one to many dependency between multiple objects. When the state of an object changes, all objects that depend on it are notified and automatically updated. This mode is sometimes called publish subscribe mode and model view mode. It is an object behavior mode.

Observer mode is an object behavior mode. Its main advantages are as follows:

  • It reduces the coupling relationship between the target and the observer, which is an abstract coupling relationship. Comply with the principle of dependency inversion.
  • A trigger mechanism is established between the target and the observer.

Its main disadvantages are as follows:

  • The dependency between the target and the observer is not completely released, and circular references may occur.
  • When there are many observers, the notification of the press conference takes a lot of time, which affects the efficiency of the program.

Life case list

For example: the leader created a group called fishing vanguard for the notice of league construction. The leader released a message that night - the new employees of league construction need to prepare performances. At this time, everyone in the group can see this message. Only the new employees will have a series of preparations for the performance after receiving this notice. However, this message has no impact on the old employees.

This is a life case of the observer model. When the leader has something to do, issue a notice to the group, and all the people in the group will do the corresponding things after receiving the notice.
The above cases can be divided into the following roles:

  • Listener (or observer): everyone in the group is a listener.
  • Manager (corresponding to the topic subject in other tutorials): that is, group. It mainly has the functions of adding (group member) listeners, removing (group member) listeners, and notifying all listeners.
  • Event (or notice): that is, the message sent by the leader to the group is an event (or notice).

Use the above example to sort out the idea of implementing an observer mode.
Take a look at a process: the leader creates a group, adds relevant personnel to the group, and then issues a notice to the group. After seeing this message, everyone in the group will do the corresponding things. When this message has nothing to do with themselves, they will do nothing.
The leader can be regarded as a thread of the application program, just a unit of program execution.
Next is the routine of general design patterns, in order to expand the program. The above roles need to be defined as abstract concepts. There are two kinds of abstractions defined in Java, one is interface and the other is abstract class. Whether it is defined as an interface or an abstract class depends on the actual situation.

abstract conception

Why define it as abstract? Let's first understand the concept of abstraction. I understand that abstraction is the definition of the common part of a class of things. For example, fruit is the abstract definition of a kind of things. When it comes to fruit, you can certainly associate it with juicy and the main taste is sweet and sour. Edible plant fruits are rich in nutrients. This is the common ingredient of fruits, but there are many kinds of fruits, including pitaya, passion fruit.
Abstract benefits: for example, there is only one kind of fruit in your family today - pitaya. If your father asks you to eat some fruit, you can certainly bring the only fruit in your family, pitaya, to honor your father. In this process, your father's fruit, rather than pitaya, can say one word less, so as to save energy and live one nanosecond more. Then we can draw a conclusion - using abstract concepts can prolong life →_ →.
Just kidding, let's get down to business. Let me talk about the benefits of abstraction:

  • When only one implementation class is defined in the interface, it is convenient to replace the functions (change an implementation class and add new functions in the new implementation class, so as to avoid changes to the caller and the original code of the original implementation class).
  • Method parameters are defined as abstractions. At this time, different implementation classes can be passed in, and the method can realize different functions.
  • Unified management makes the program more standardized. When new non abstract methods are defined in the abstraction, subclasses can be inherited and used directly.

With the above foreshadowing, it is easy to understand the following code example.

Observer mode code example

Code address: https://gitee.com/kangarookin...
Make up business: after users buy goods, they use the observer mode to add points to the corresponding users. When the user Member expires, use the observer mode to send text messages to the corresponding users.
Note: the business here is fabricated, and the end will provide you with several scenarios for the use of observer mode in real enterprises.

The observer mode is also a publish subscribe mode.
Different observers need different implementation methods, so first create a manager interface and define it as an abstract concept to facilitate subsequent expansion.
This interface is equivalent to - group (Manager)

/**
 * Top level interface of observer
 * @param <T>
 */
public interface ObserverInterface<T> {
    //Register listener
    public void registerListener(T t);
    //Remove listener
    public void removeListener(T t);
    //Notify listener
    public void notifyListener(DataEvent t);
}

Define an abstract listener interface
This interface is equivalent to - group member (listener)

/**
 * Listener The top-level interface exists to abstract the Listener
 */
public interface MyListener {
    void onEvent(DataEvent event);
}

Define abstract event interfaces
This interface is equivalent to the notification published in the group

@Data
public abstract class DataEvent {
    private String msg;
}

Create a manager's implementation class, which is equivalent to a specific group (such as wechat group and nailing group)

/**
 * Observer of loop call mode (synchronous)
 */
@Component
public class LoopObserverImpl implements ObserverInterface<MyListener> {
    //Registration list of listeners
    private List<MyListener> listenerList = new ArrayList<>();
    @Override
    public void registerListener(MyListener listener) {
        listenerList.add(listener);
    }

    @Override
    public void removeListener(MyListener listener) {
        listenerList.remove(listener);
    }

    @Override
    public void notifyListener(DataEvent event) {
        for (MyListener myListener : listenerList) {
            myListener.onEvent(event);
        }
    }
}

Create two implementation classes of event, one is integral event and the other is short message event

/**
 * Integral event class
 */
public class ScoreDataEvent extends DataEvent {
    private Integer score;
}

/**
 * SMS events
 */
public class SmsDataEvent extends DataEvent {
    private String phoneNum;
}

Create two implementation classes of listener, one for integral processing and the other for short message processing

/**
 * MyListener Implementation class, score listener
 */
@Component
public class MyScoreListener implements MyListener {
    @Override
    public void onEvent(DataEvent dataEvent) {
        if (dataEvent instanceof ScoreDataEvent) {
            //... Omit business logic
            System.out.println("Integral processing:" + dataEvent.getMsg());
        }
    }
}

/**
 * MyListener Implementation class, SMS listener
 */
@Component
public class MySmsListener implements MyListener {
    @Override
    public void onEvent(DataEvent dataEvent) {
        if (dataEvent instanceof SmsDataEvent) {
            //... Omit SMS processing logic
            System.out.println("SMS processing");
        }
    }
}

The elements of the observer mode are all here. Let's run in the main method

public class Operator {
    public static void main(String[] args) {
        //Through spring's AnnotationConfigApplicationContext, com example. demo. user. admin. All spring annotated classes in the design path are scanned and put into the spring container
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("com.example.demo.user.admin.design");
        //Get the instance of the corresponding bean from the spring container
        LoopObserverImpl loopObserver = context.getBean(LoopObserverImpl.class);
        MyScoreListener scoreL = context.getBean(MyScoreListener.class);
        MySmsListener smsL = context.getBean(MySmsListener.class);

        //Register listener with observer
        loopObserver.registerListener(scoreL);
        loopObserver.registerListener(smsL);
        ScoreDataEvent scoreData = new ScoreDataEvent();
        scoreData.setMsg("Loop synchronization observer");
        //Publish the score event and notify the listener
        loopObserver.notifyListener(scoreData);

        /*******************************************/
        //Get QueueObserverImpl observer from spring container
        
        QueueObserverImpl queueObserver = context.getBean(QueueObserverImpl.class);
        //Register listener with observer
        queueObserver.registerListener(scoreL);
        queueObserver.registerListener(smsL);
        ScoreDataEvent scoreData1 = new ScoreDataEvent();
        scoreData1.setMsg("Queue asynchronous observer");
        //Publish the score event and notify the listener
        queueObserver.notifyListener(scoreData1);
    }
}

Next, let's take a look at the difference between the following new observer implementation class and the observer implementation class LoopObserverImpl in the above example

/**
 * Starting a thread loop to block the observer of the queue can realize decoupling asynchrony.
 */
@Component
public class QueueObserverImpl implements ObserverInterface<MyListener> {
    //Registration list of listeners
    private List<MyListener> listenerList = new ArrayList<>();
    //Create a blocking queue of size 10
    private BlockingQueue<DataEvent> queue = new LinkedBlockingQueue<>(10);
    //Create a thread pool
    private ExecutorService executorService = new ScheduledThreadPoolExecutor(1, r -> {
        Thread t = new Thread(r);
        t.setName("com.kangarooking.observer.worker");
        t.setDaemon(false);
        return t;
    });
//    private ExecutorService executorService = Executors.newFixedThreadPool(1);

    @Override
    public void registerListener(MyListener listener) {
        listenerList.add(listener);
    }

    @Override
    public void removeListener(MyListener listener) {
        listenerList.remove(listener);
    }

    @Override
    public void notifyListener(DataEvent event) {
        System.out.println("Put into queue DataMsg: " + event.getMsg());
        queue.offer(event);
    }

    @PostConstruct
    public void initObserver() {
        System.out.println("Start a thread on initialization");
        executorService.submit(() -> {
            while (true) {
                try {
                    System.out.println("Loop to get data from the blocking queue, take If there is no data in the blocking queue, it will be blocked");
                    DataEvent dataMsg = queue.take();
                    System.out.println("Get data from blocking queue:" + dataMsg.getMsg());
                    eventNotify(dataMsg);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    private void eventNotify(DataEvent event) {
        System.out.println("Loop all listeners");
        for (MyListener myListener : listenerList) {
            myListener.onEvent(event);
        }
    }
}

The difference is that the blocking queue is introduced to turn the notification operation into an asynchronous operation, which can be returned directly after putting the event time into the blocking queue. Not like LoopObserverImpl, you can't return until the listener registry cycle is completed. In this way, the decoupling and asynchrony of notification operation and circular listener registry are realized.

Give an example to illustrate the difference between asynchronous implementation and synchronous implementation:
Synchronization: it's also an example of group building. If the leader is a nanny leader, he may not feel at ease after notifying the task. He has to ask one by one, Xiao Zhang, what performance are you going to perform, and how long can you prepare the duck. Xiao Hong, what about you_ →. . .
Asynchrony: if it's a shopkeeper type leader, he doesn't care after publishing the news.
The above is the difference between synchronization and asynchrony. Synchronization means that the leader is a nanny. After asking one by one to understand the situation, this thing is finished. Asynchrony is when the leader releases the news.

Implementation of open source framework

Synchronization mode

spring's publishing and subscription is based on the synchronous observer mode: in short, it is to register all listeners in a list, and then when publishing events, call the onEvent method of each listener in the loop through the loop listener list. Each listener determines whether the incoming event belongs to the currently required event in the onEvent method, If it belongs to, the event will be handled; otherwise, it will not be handled.

spring's applicationeventmulticast is the top-level interface of the observer in the example

ApplicationListener is the listener top-level interface of the sample code

registerListeners() called in the refresh method; The method is to register all listener implementation classes in the observer registry

The multicastEvent method of applicationeventmulticast is the notification method mentioned above. Here is the circular listener registry and calls the onApplicationEvent method of each listener (the invokeListener method here will eventually call listener.onApplicationEvent(event);)

Just look at the implementation of an onApplicationEvent method. Is it very similar to the above example

Asynchronous mode

Observer mode is used in many places in nacos, such as establishing a connection between client and server, publishing connection events, relevant listeners doing corresponding processing, and disconnecting the connection is the same.

After the server side receives the registration request from the client side, it will issue a registration event notification

When the Nacos server is started, a thread will be started to do an endless loop to take the data in the queue. If there is no data, it will be blocked. Therefore, the dead loop will continue to loop only when there is always data in the queue. When there is no data in the queue, it will be blocked in the queue take(); Method division.

Let's take a look at receiveEvent(event); What is done in the method reflects the subtlety of the design in the framework: in our own design above, it should be necessary to call the onApplicationEvent method of all listeners circularly, but when there are too many listeners in the registry, there will be a problem that the circular call is too slow (some events may have multiple listeners to deal with), The multithreaded processing mode is used here to make these calls processed in parallel, which greatly improves the event processing efficiency of the framework.

About business usage scenarios

It can be said that the observer mode can be solved, and the message queue can also be solved, and can be done better. The choice is mainly based on the actual situation.

When the company has sufficient server resources, a large number of users, frequent calls to relevant business logic, high reliability of messages, and more flexible publishing and subscription of messages, you can consider using message queue.

When the server resources are insufficient, or there are few calls, or you want to use a lightweight notification mechanism, and the requirements for message reliability are not high, you can consider using the observer mode in the project code.
Of course, the trouble of using the observer mode is to write a certain amount of code by yourself, and the function is not as powerful as the message queue, and the reliability of the message cannot be guaranteed. When the observer obtains the message and generates an exception in his own processing logic, You may also need to write your own degradation code after an exception occurs (of course, it is not necessary for business scenarios that do not require high reliability).

Why does the framework use observer mode instead of message queue (personal understanding):

  1. The message queue is too heavy;
  2. Itself is an open source framework (the province represents the original), which is not suitable for introducing another heavy message queue. Increasing the cost and difficulty of users' use and deployment is also unfavorable to their own promotion.

summary

In fact, I want to tell you that design mode is actually just a way of thinking. We learn design mode only to understand a basic programming way of thinking, which needs to change according to the actual situation in the actual use process. The same is true of observer mode. As long as the thought does not decline, you can create many observer modes with different implementation methods .

I'm kangarooking, maintaining articles in an open source way. I hope you will give us your advice and actively participate_ (: з」 ∠)_.

Topics: Java Design Pattern