7

One, the observer mode

1. What is the observer pattern

When there is a one-to-many dependency relationship between objects, the state of one of the objects changes, and all objects that depend on it will be notified. This is the observer pattern.

2. Actual scene

1. DOM event

During the development process, the most common observer mode scenario is the DOM event function. First look at the code:

document.body.addEventListener('click', () => {
    alert(2)
}, false)

When the body node is clicked, alert(2) is triggered. From the observer mode, we subscribe to bdoy node. When the click event is triggered, we will receive a notification.

2. Website Login

The website login function must be implemented by most students who have done platform requirements. When different modules in the website, such as Header module, Nav module, and text module, all rely on the user data obtained after login, how to implement it?

2.1 Common practice

Look at the code first:

login.succ((data => {
    header.setAvatar(data.avatar) // 设置头像
    nav.setAvatar(data.avatar) // 设置导航区的头像
    list.refresh() // 刷新列表
})

Is this kind of code particularly familiar? Put the dependent method in the callback function. The above is the method of adding each module in the callback function of successful login. This results in a high degree of coupling between each module and the login module. When a new address bar module is added, the callback function of the login module has to be modified again, which violates the open-closed principle.

2.2 Observer Mode

Use the observer mode to optimize the above requirements.

The login module is a subscription object. The Header module, Nav module, and text module add subscriptions to the login module. When the login module changes, it will notify each module that has subscribed to the login module. code show as below:

// 登录模块js
// 登录成功后,发布“loginSucc”登录成功消息,并传递data数据
login.succ(data=> {
    login.trigger('loginSucc', data)
})

// header模块js
// 订阅“loginSucc”登录成功消息
login.listen('loginSucc', () => {
    header.setAvatar(data.avatar)
})

// nav模块js
// 订阅“loginSucc”登录成功消息
login.listen('loginSucc', () => {
    nav.setAvatar(data.avatar)
})

The above code uses the observer mode to reconstruct the website login function. No matter how many new business modules are added and the login function is relied on in the future, it is only necessary to add a subscription for successful login in the module without changing the login module.

3. Two-way data binding

Two-way data binding can also be achieved through the observer pattern.

Two-way refers to the view view and the model model . When the view changes, the model also changes. Similarly, when the model changes, the view changes synchronously.

Divided into the following steps to achieve:

3.1 Create a new publish-subscribe object

Create a new publish-subscribe object for publishing and subscribing to messages.

  • subscribe : Subscription function, when other objects add subscription messages, add the callback function push into the callbacks object array;
  • publish : Publish function, when a message is published, trigger callbacks corresponding to the message in callback .
const Pubsub = {
    subscribe: function (ev, callback) {
        this._callbacks || (this._callbacks = {});
        (this._callbacks[ev] || (this._callbacks[ev] = [])).push(callback);
    },

    publish: function () {
        const args = [...arguments]
        const ev = args.shift()

        if (!this._callbacks) return
        if (!this._callbacks[ev]) return

        this._callbacks[ev].forEach(callback => {
            callback(...args)
        })
    }
}
3.2 ui update
3.2.1 Release ui update message

Registration document of keyup / change event, when the activation event dom element has data-bind time attribute, that ui being updated release ui update messages notify subscribers.

function eventHander (e) {
    const { target } = e
    const { value: propValue } = target

    const propNameWhole = target.getAttribute('data-bind')
    if (propNameWhole) {
        // 发布ui更新消息
        Pubsub.publish('ui-update-event', { propNameWhole, propValue })
    }
}

console.log(document.addEventListener)
document.addEventListener('change', eventHander, false)
document.addEventListener('keyup', eventHander, false)
3.2.2 Subscribe to model update news

All dom elements that data-bind attributes model update message. When model updated, ui will be notified.

// 订阅model更新消息,更新后所有符合条件的dom节点都会收到通知,进行更新
Pubsub.subscribe('model-update-event', function ({propNameWhole, propValue}) {
    const elements = document.querySelectorAll(`[data-bind="${propNameWhole}"]`)

    elements.forEach(element => {
        const elementTagName = element.tagName.toLowerCase()
        const formTypeTagNames = ['input', 'select', 'textarea']
        if (formTypeTagNames.includes(elementTagName)) {
            element.value = propValue
        } else {
            element.innerHTML = propValue
        }
    })
})
3.3 model update
3.3.1 Subscribe to ui update news

Subscribe ui update message, when ui updating, triggered modal update.

class Bind {
    constructor () {
        this.modelName = ''
    }

    initModel ({ modelName }) {
        this.modelName = modelName

        // 订阅ui更新消息
        Pubsub.subscribe('ui-update-event', ({propNameWhole, propValue}) => {
            const [ , _propName] = propNameWhole.split('.')
            this.updateModalData(_propName, propValue)
        })
    }
    
    // xxx省略xxx

    updateModalData (propName, propValue) {
        const propNameWhole = `${this.modelName}.${propName}`
        // 发布model更新消息
        Pubsub.publish('model-update-event', { propNameWhole, propValue });
    }

}
3.3.2 Publish model update message

model time updates, model update message, this time, subscribed to model update message ui , will be notified.

class Bind {
    constructor () {
        this.modelName = ''
    }
    
    // xxx省略xxx

    loadModalData (modelData) {
        for (let propName in modelData) {
            this.updateModalData(propName, modelData[propName])
        }
    }

    updateModalData (propName, propValue) {
        const propNameWhole = `${this.modelName}.${propName}`
        // 发布model更新消息
        Pubsub.publish('model-update-event', { propNameWhole, propValue });
    }

}

3. Summary

As can be seen from the actual scenario examples above, the observer mode establishes a set of trigger mechanisms to help us complete loosely coupled code writing. But it should not be overused, otherwise the program will be difficult to track and understand.

Second, the decorator mode

1. What is the decorator pattern

Simply put, the decorator mode is to dynamically add functionality to the object.

There is a duck object, it will sound makeVoice and sleep sleep , but because it is too small to walk, the code is as follows:

const duck =  {
    makeVoice: () => {
        console.log('我会嘎嘎嘎啦')
    },
    sleep: () => {
        console.log('谁又不会睡觉呢')
    },
    init: function () {
        this.makeVoice()
        this.sleep()
    }
}

duck.init()

When it was 3 months old, it suddenly learned to walk walk . At this time, how to add the function of walk duck In most cases, we will choose to directly modify the duck duck method, the code is as follows:

const duck =  {
    makeVoice: () => {
        console.log('我会嘎嘎嘎啦')
    },
    sleep: () => {
        console.log('谁又不会睡觉呢')
    },
    walk: () => {
        console.log('哈哈哈,我会走路啦')
    },
    init: function () {
        this.makeVoice()
        this.sleep()
        this.walk()
    }
}

duck.init()

Happy time is always short. The longer the duck grows, the more functions it has. One day, you take time off to play and ask your friend to take care of this duck. Unfortunately, the duck is about to lay eggs. Your friend needs to add an egg laying function to the duck. This is a bit troublesome because this is the first time he takes care of this duck. Duck, he was worried about what would be affected by adding methods directly inside the duck.

So he thought of a good way, instead of directly modifying the internals of the duck, but using an external function to reference the duck and adding the egg-laying function to the external function.

code show as below:

const before = function (fn, beforeFn) {
    return function () {
        beforeFn.apply(this, arguments)
        return fn.apply(this, arguments)
    }
}

const after = function (fn, afterFn) {
    return function () {
        const __ = fn.apply(this, arguments)
        afterFn.apply(this, arguments)
        return __
    }
}

const duck =  {
    makeVoice: function () {
        console.log('我会嘎嘎嘎啦')
    },
    sleep: function () {
        console.log('谁又不会睡觉呢')
    },
    walk: function () {
        console.log('哈哈哈,我会走路啦')
    },
    init: function () {
        this.makeVoice()
        this.sleep()
        this.walk()
    }
}

after(duck.init, function egg () {
    console.log('生蛋快乐~')
}).apply(duck)

This is the decorator mode, which dynamically adds functionality to the duck without directly modifying the duck itself.

2. Actual scene

1. Data reporting

The data reporting of custom events generally depends on the click event, so this click event has to undertake the original function as well as the data reporting function.

1.1 Common practice

First on the code:

const loginBtnClick = () => {
    console.log('去登录')
    console.log('去上报')
} 

It seems that there is nothing wrong with this kind of code, which can be seen everywhere in the project, and evasion (procedure-oriented programming) is shameful but useful.

1.2 Decorator pattern practice

The above code can be reconstructed through the decorator pattern, the responsibilities are divided more finely, the code is loosely coupled, and the reusability is higher.


const after = function (fn, afterFn) {
    return function () {
        const __ = fn.apply(this, arguments)
        afterFn.apply(this, arguments)
        return __
    }
}

const showLogin = function () {
    console.log('去登录')
}

const log = function () {
    console.log('去上报')
}

const loginBtnClick = after(showLogin, log)

loginBtnClick()

2. Dynamically increase parameters

A conventional ajax request parameters include type / url / param , a special case when the burst, the need ajax parameters, add a token parameter.

2.1 Common practice

First on the code:

const ajax = function (type, url, param) {
    // 新增token参数
    param.token = 'xxx'
    // ...ajax请求...省略
}

Well, once again violated the open-closed principle, directly modified the ajax function of 060def1c001e94.

2.2 Decorator approach

Through the decorator mode, before ajax called, add the token ajax , the code is as follows:

const before = function (fn, beforeFn) {
    return function () {
        beforeFn.apply(this, arguments)
        return fn.apply(this, arguments)
    }
}

let ajax = function (type, url, param) {
    console.log(arguments)
    // ...ajax请求...省略
}

ajax = before(ajax, function (type, url, param) {
    console.log(param)
    param.token = 'xxx'
})

ajax('type', 'url', {name: 'tj'})

This reduces ajax responsibility function, improved ajax function of reusability,

3. Summary

This article describes the application scenarios of the decorator mode and the benefits it brings to us through three examples of dynamically adding functions, reporting data, and dynamically adding parameters to the duck function.

The decorator mode makes the object more stable and easy to reuse. The unstable functions can be dynamically added during personalization.

3. Chain of Responsibility Model

1. What is the chain of responsibility model

Chain of Responsibility pattern is when an object a , there are several possible request object b , c , d , e when we b , c , d , e define a mandate to form a chain of responsibility, so a just need to find b Initiate a request, and then continue the request along the chain of responsibility until an object is found to handle a .

Girls like to eat together, I am looking for someone to eat together, the code is as follows:

Hmm.... Female programmers are indeed like this. They have to write code and send requests after dinner.
const [ astrid, brooke, calliope ] = [{
    name: 'astrid',
    requirement: '我要吃湘菜'
},{
    name: 'brooke',
    requirement: '我要找10个人一起吃饭'
},{
    name: 'calliope',
    requirement: '我要和男朋友一起吃饭'
}]

// 是否满足Astrid的要求
function isSatisfyAstrid (user) { 
    // ... 省略... 
}

// 是否满足Brooke的要求
function isSatisfyBrooke (user) { 
    // ... 省略... 
}

// 是否满足Calliope的要求
function isSatisfyCalliope (user) { 
    // ... 省略... 
}

function eatDinner () {
    if (isSatisfyAstrid()) {
        console.log(`我可以和 astrid 一起吃晚饭啦`)
    } else if (isSatisfyBrooke()) {
        console.log(`我可以和 brooke 一起吃晚饭啦`)
    } else if (isSatisfyCalliope()) {
        console.log(`我可以和 calliope 一起吃晚饭啦`)
    } else {
        console.log(`哎呀,我要一个人吃晚饭啦`)
    }
}

Since astrid , brooke , calliope different requirements for dinner, I need to initiate dinner requests one by one until I find someone who agrees to have dinner with me.

Here, I assume that astrid is to eat Hunan brooke , the requirement of 060def1c0023de is to find 10 people to join a table to eat, and calliope is to only want to eat with her boyfriend.

The above code using if-else is very rigid. If I have another friend davi , I must modify the eatDinner method again, which violates the open-closed principle and is not easy to maintain.

The following uses the chain of responsibility to optimize the above code, the code is as follows:

// 给每个人定义一个职责
const chainOrderA = new Chain(isSatisfyAstrid)
const chainOrderB = new Chain(isSatisfyBrooke)
const chainOrderC = new Chain(isSatisfyCalliope)

// 设置一下职责链的顺序
chainOrderA.setNextSuccessor(chainOrderB)
chainOrderB.setNextSuccessor(chainOrderC)

// 发起请求,这时我只需要向职责链上的第一个人请求
function eatDinner () {
    chainOrder.passRequest() // 发起请求
}

Transfer the responsibility to the Chain function, and define the next responsibility function of the responsibility setNextSuccessor chainOrderA -> chainOrderB -> chainOrderC responsibility chain. At this time, I only need to astrid . If the request fails, it will follow Continue to request along the chain of responsibility until I find someone to have dinner with me.

The following will describe how to use the chain of responsibility mode in actual scenarios and how to implement the Chain method. Please continue to look down.

2. Actual scene

1. 618 pre-sale merchandise orders

E-commerce websites are not allowed to launch product pre-sale activities. Assuming that before 618, if you pay a 500 deposit in advance, you will get a 100 yuan coupon, and you will get a 200 yuan deposit and you will get a 50 coupon. If you don’t pay the deposit, you won’t have a coupon. The purchase events on the day of 618 are as follows:

1.1 Common practice

Code first.

The code in this article is just an example and has nothing to do with business.
const order = function (orderType) {
    if (orderType === 500) {
        console.log('已预付500定金,享有100优惠券')
    } else if (orderType === 200) {
        console.log('已预付200定金,享有50元优惠券')
    } else {
        console.log('未付定金,无优惠')
    }
}

order(500) // '已预付500定金,享有100优惠券'

Familiar code, a long section of if-else judgment, is not conducive to maintenance.

1.2 Chain of Responsibility Model

Define a responsibility class Chain .

  • Receive a responsibility function fn as a parameter;
  • setNextSuccessor specifies the next responsibility function of the responsibility;
  • passRequest initiates a request for the responsibility function fn

    • If the returned result is nextSuccesstor , the request failed, and continue to request the next responsibility function in the responsibility chain;
    • If it does not return nextSuccesstor , it means that the object receiving the request has been found, the request result is returned, and the next responsibility function in the responsibility chain is no longer executed.

code show as below:

const Chain = function(fn) {
  this.fn = fn;
  this.successor = null;
};

Chain.prototype.setNextSuccessor = function(successor) {
  return this.successor = successor;
}

Chain.prototype.passRequest = function() {
  const ret = this.fn.apply(this, arguments)
  if (ret === 'nextSuccessor') {
    return this.successor && this.successor.passRequest.apply(this.successor, arguments);
  }
  return ret;
};

Then define the responsibility class instance, and setNextSuccessor , the code is as follows:

const order500 = function (orderType) {
  if (orderType === 500) {
      console.log('已预付500定金,享有100优惠券')
  } else {
    return 'nextSuccessor'
  }
} 

const order200 = function (orderType) {
  if (orderType === 200) {
    console.log('已预付200定金,享有50元优惠券')
  } else {
    return 'nextSuccessor'
  }
} 

const chainOrder500 = new Chain(order500)

const chainOrder200 = new Chain(order200)

chainOrder500.setNextSuccessor(chainOrder200)

chainOrder500.passRequest(200)

The above code chainOrder500 and chainOrder200 into a responsibility chain. Regardless of the type of user, it only needs to initiate a request chainOrder500 chainOrder500 cannot process the request, it will continue to initiate the request along the responsibility chain until it finds a responsibility method that can handle the request. .

Through the chain of responsibility model, the complex relationship between the request sender and multiple receivers is decoupled. It is no longer necessary to know which receiver will receive the sent request, and only need to initiate a request to the first stage of the responsibility chain.

3. Summary

The chain of responsibility pattern helps us manage the code and reduces the coupling between the requesting object and the receiving request object.

The number and order of nodes in the responsibility chain model can be changed freely, and which nodes can be included in the chain can be determined at runtime.

Practical exercises can be carried out through github source code

Hope it can be helpful to you, thanks for reading ❤️


wonderful · 160def1c002f0a

[Simple and easy to understand design pattern (on)]


Welcome to follow the blog of Bump Lab: aotu.io

Or follow the AOTULabs official account (AOTULabs) and push articles from time to time.


凹凸实验室
2.3k 声望5.5k 粉丝

凹凸实验室(Aotu.io,英文简称O2) 始建于2015年10月,是一个年轻基情的技术团队。