Take a look at the observer pattern in the previous article. Published-subscribe and observer patterns are essentially the same, and the published-subscribe pattern does not appear in the classic design patterns book GoF, and in many places they are considered one design pattern.

GoF’s name also has an interesting story, posted here:

The authors of the DesignPatternsBook came to be known as the “Gang of Four.” The name of the book (“Design Patterns: Elements of Reusable Object-Oriented Software”) is too long for e-mail, so “book by the gang of four” became a shorthand name for it. After all, it isn’t the ONLY book on patterns. That got shortened to “GOF book”, which is pretty cryptic the first time you hear it.

Back to the text.

The code has been written for several years, and the design pattern is in a state of forgetting and forgetting. Recently, I have some feelings about the design pattern, so I will learn and summarize it again.

Most speak design pattern articles are using Java, c + + such based on the class the static type of language, as a front-end developer, js this prototype based dynamic language, function has become a first class citizen, slightly different on some design patterns, even simple to don’t like to use the design patterns, sometimes also can produce some confusion.

The following are summarized in the order of “scenario” – “design pattern definition” – “code implementation” – “general”. If there are any improper points, welcome to discuss.

scenario

Suppose we are developing a food delivery site. When entering the site, the first step is to request the back-end interface to get the user’s common food delivery address. Then go to the other interface and render the page. If observer mode is used, it might read something like this:

// There are three modules in the page: A, B, and C
// Modules A, B, and C are all written by different people and provide different methods for us to call
const observers = []
// Register the observer
observers.push(A.update)
observers.push(B.next)
obervers.push(C.change)



// getAddress asynchronous request
getAddress().then(res= > {
  const address = res.address;
  observers.forEach(update= > update(address))
})
Copy the code

The getAddress module is decouple with the other A, B, and C modules, but you still need to maintain an array that is adept at registering observers, and know what methods each module provides for callbacks.

We can use the publish-subscribe model to decouple the getAddress module from the other A, B, and C modules more thoroughly.

Design Pattern definition

Recall the observer model:

The observer pattern is a software design pattern in which an object, named the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods.

In observer mode, the Subject maintains its own list of observers for registration and notification.

In the publish-subscribe model, introducing an intermediate platform for registration and notification acts as a decoupling from Subject.

The observer registers events with EventBus via ON, and the Subject emits events to EventBus via emit, which updates the observer.

Next, implement a simple EventBus.

// event.js
const observersMap = {}
const listen = function (key, fn) {
    if(! observersMap[key]) { observersMap[key] = []; } observersMap[key].push(fn); };const trigger = function () {
    const key = Array.prototype.shift.call(arguments),
        fns = observersMap[key];
    if(! fns || fns.length ===0) {
        return false;
    }
    for (let i = 0, fn; fn = fns[i]; i++ ) { fn.apply(this.arguments); }};const remove = function (key, fn) {
    const fns = observersMap[key];
    if(! fns) {return false;
    }
    if(! fn) { fns && (fns.length =0) // Empty all
    } else {
        let findIndex = -1;
        for (let i = 0; i < fns.length; i++) {
            if (fns[i] === fn) {
                findIndex = i;
                break; }}if(findIndex ! = = -1) {
            fns.splice(findIndex, 1); }}};// The same function may see different names, which are exported here
export const EventBus = {
    listen,
    attach: listen,
    on: listen,

    remove,
    detach: remove,

    trigger,
    emit: trigger,
};
Copy the code

We use observersMap to save different events into different arrays, and then we can call the corresponding arrays when emit. Here’s an example:

import { EventBus } from "./event.js";

const WindLiang = {
    writePost(p) {
        EventBus.emit("windliang", p); }};const XiaoMing = {
    update(post) {
        console.log("I got it." + post + "And gave a thumbs-up."); }};const XiaoYang = {
    update(post) {
        console.log("I got it." + post + "And forwarded it."); }};const XiaoGang = {
    update(post) {
        console.log("I got it." + post + "And collect."); }}; EventBus.on("windliang", XiaoMing.update);
EventBus.on("windliang", XiaoYang.update);
EventBus.on("windliang", XiaoGang.update);

WindLiang.writePost("New article - Observer Model, Balabala.");
Copy the code

Code implementation

Let’s modify the observer mode code at the beginning:

Address module:

import { EventBus } from "./event.js";


// getAddress asynchronous request
getAddress().then(res= > {
  const address = res.address;
  EventBus.emit('ADDRESS', address)
})
Copy the code

A module

import { EventBus } from "./event.js";

const update = (address) = > {
  // Own logic
}

EventBus.on('ADDRESS'.(address) = > update(address))
Copy the code

B module

import { EventBus } from "./event.js";

const next = (address) = > {
  // Own logic
}

EventBus.on('ADDRESS'.(address) = > next(address))
Copy the code

C module

import { EventBus } from "./event.js";

const change = (address) = > {
  // Own logic
}

EventBus.on('ADDRESS'.(address) = > change(address))
Copy the code

As you can see, the getAddress module no longer needs to care who the observer is, it just fires the update event to the EventBus.

If each module needs address information internally, it simply subscribes to the event and passes in the callback function.

A special case

In practical projects, there may be some special cases where emit is usually executed in an asynchronous event. If the asynchronous event is suddenly executed faster, an event may emit before a module executes on.

At this point, we can rewrite EventBus to make it possible to subscribe to events and then trigger them.

In order not to change the original logic, we can use proxy mode to rewrite.

// eventProxy.js

import { EventBus as EventBusOriginal } from "./event.js";

let offlineStack = []; // listen to the emit event before it is cached

const triggerProxy = function () {
    const _self = this;
    const args = arguments;
    const fn = function () {
        return EventBusOriginal.trigger.apply(_self, args);
    };
    if (offlineStack) {
        return offlineStack.push(fn);
    }
    return fn();
};
const listenProxy = function (key, fn) {
    EventBusOriginal.listen(key, fn);
    if(! offlineStack) {return;
    }
    for (let i = 0, fn; (fn = offlineStack[i]); i++) {
        fn();
    }
    offlineStack = null;

};

const listen = listenProxy || EventBus.listen;
const trigger = triggerProxy || EventBus.trigger;

export constEventBus = { ... EventBusOriginal, listen,attach: listen,
    on: listen,

    trigger,
    emit: trigger,
};

Copy the code

In trigger, if offlineStack is not null, listen has not been called and the current event is saved.

Listen iterates through previously saved events and sets offlineStack to null to indicate that LISTEN has been called.

Take a look at the results:

import { EventBus } from "./eventProxy.mjs";

const WindLiang = {
    writePost(p) {
        EventBus.emit("windliang", p); }};const XiaoMing = {
    update(post) {
        console.log("I got it." + post + "And gave a thumbs-up."); }};const XiaoYang = {
    update(post) {
        console.log("I got it." + post + "And forwarded it."); }};const XiaoGang = {
    update(post) {
        console.log("I got it." + post + "And collect."); }}; WindLiang.writePost("New article - Observer Model, Balabala.");

EventBus.on("windliang", XiaoYang.update); // I received the new article - Observer mode, Balabala and forwarded it
Copy the code

Although it was on after emit first, it still executed normally.

The above solution is very crude and only applies to scenarios with one event and only one ON, otherwise such as the following:

WindLiang.writePost("New article - Observer Model, Balabala.");

EventBus.on("windliang", XiaoMing.update);
EventBus.on("windliang", XiaoYang.update);
EventBus.on("windliang", XiaoGang.update);
Copy the code

Only XiaoMing. Update will emit listen and the first emit will be missed because listen clears the cache.

Or had an on before writePost:

EventBus.on("windliang", XiaoMing.update);
WindLiang.writePost("New article - Observer Model, Balabala.");

EventBus.on("windliang", XiaoYang.update);
EventBus.on("windliang", XiaoGang.update);
Copy the code

Update listen will emit listen only for XiaoMing. Update listen will emit listen only for XiaoMing.

For the actual scenario, we need to continue to make adjustments based on the situation.

The total

The publisk-and-subscribe pattern completely decouples Subject and Observers from the original Observer pattern. The Subject no longer cares about who is subscribed to it, and Observers only need to subscribe internally to the events they care about.

The encapsulation of EventBus also allows for better reuse, eliminating the need for each module to maintain its own list of observers.

At the same time, however, it has the disadvantage that all event subscriptions are scattered across modules, and there is no global perspective on which modules subscribe to an event, which can make the program difficult to understand and debug.

More design patterns recommended reading:

Front end design mode series – strategic mode

Front-end design mode series – proxy mode

Front end design mode series – decorator mode

Front end design mode series – Observer mode


Thank you for reading till now. Today is New Year’s Eve. Because of the epidemic, I didn’t go back home this year.

Send a red envelope to thank you for your continuous support. You can reply “Happy New Year” in the background of the public account “Windliang” to get the lucky draw link.

Finally, HAPPY New Year to you all!