设计模式(-)

1. 设计模式之观察者模式

观察者模式,也叫发布订阅模式。它是由1个发布者和多个订阅者组成,它解决了主体对象与多个观察者之间的耦合。

其实发布订阅模式在日常生活中例子很多:比如我们在网络购物时,如果挑选到自己喜欢的商品,但碰巧发现已经售罄。这时候旁边会有“关注该商品”之类的按钮。我们添加对该商品的关注。当商品库存有变化时,系统会向关注该商品的用户发送通知,不同用户收到通知采取各自的行动。在这里,通过添加关注,用户添加了对该商品的订阅;当商品库存变化时,系统作为发布者,将信息发送给之前添加关注的用户。当我们取消了对该商品的关注,那么后期当该商品库存变动时将不会给我们发送通知。


综上,一个观察者模式需要具备以下对象:发布者、订阅着;
需要具备以下方法:添加订阅、发布订阅、取消订阅


下面,我们将这个例子转换为具体的代码实现来看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
let Observer = (function () {
let _observers = {}

function add (type, fn) {
if (!_observers[type]) {
_observers[type] = [fn]
} else {
_observers[type].push(fn)
}
}

function remove (type, fn) {
if (!_observers[type]) return
let idx = _observers[type].indexOf(fn)
_observers[type].splice(idx, 1)
}

function fire (type, data) {
if (!_observers[type]) return
_observers[type].forEach(item => {
item.call(this, data)
})
}

return {
add,
remove,
fire
}
})()

Observer.add('prod1', data => {
console.log(data.stock)
})

Observer.add('prod1', data => {
console.log('还剩: ' + data.stock)
})

let sub3 = function (data) {
console.log('我是第三个订阅者:' + data.stock)
}

Observer.add('prod1', sub3)

Observer.fire('prod1', {
stock: 20
})

Observer.remove('prod1', sub3)

Observer.fire('prod1', {
stock: 40
})


除此之外,还有很多场景用到了这种模式。

比如Vue的响应式实现中,当data中的数据首次被访问时,data对应的watcher添加对该data的订阅,所有的订阅会被放在对应的dep实例中。当data变化时,通过触发dep.notify来发布订阅,通知data对应的订阅者watcher,watcher再执行相应的update函数。

我们平时买彩票时,买完彩票我们会一直关注该彩票的开奖公告,也就是添加了对该彩票的订阅。当彩票开奖时,我们会从特定渠道收到彩票开奖的信息并作出各自的反应。

生活中这样的例子太多了,不胜枚举,在这里就不多说了。


昨天本来打算把状态模式、策略模式、观察者模式以及模板模式都讲下,无奈写完观察者模式已经凌晨1点了,今天继续写喽…


2. 设计模式之策略模式和状态模式

为什么一次讲两个设计模式呢?因为这俩货长得有点像,对比着来讲讲看。

策略模式:对于同一个问题,可以采取不同的解决方式,不同的策略之间可以相互替换。
状态模式:根据不同的状态采取不同的行为。状态之间对应的行为不能相互替换。

好吧,干巴巴的概念确实不好理解,再加上我蹩脚的语言表达能力,实在不是我那意思。还是从案例切入吧。


为什么会出现这两种模式?
  1. 首先说说状态模式。在平时的开发中我们在写函数时偶尔会被各种复杂的条件判断搞得晕头转向。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function doAction(result) {
if (result === 'state1') {
// todo state1
} else if (result === 'state2') {
// todo state2
} else if (result === 'state3') {
// todo state3
} else {
// todo default
}
}

// 或者

function doAction(result) {
switch(result) {
case 'state1':
// todo state1
break;
case 'state2':
// todo state2
break;
case 'state3':
// todo state3
break;
default:
break;
}
}

乍一看还可以啊。的确,面对当前的需求,确实够用了。如果这时候产品经理要加状态呢,比如要加个state4,state5,state6…哎呀妈哎,产品经理,你是魔鬼么?

斗图啊

不过吐槽归吐槽,产品经理的工作就是定需求,改需求,和程序员日常撕逼。无论如何,代码还是要改的。

其实吧,上边的代码是我们经常见到的,早就习以为常了。
然而它的局限性在于 扩展性不好。每当状态增删改时,都要改写函数体。改得越多,就越有可能出bug。

这时候我们的状态模式就闪亮登场了

我们的思路是把状态全部抽出来,做一个STATES_MAP,用于统一维护状态和对应行为。在状态需求发生变更
时,只需要改动STATES_MAP就可以啦,而不用改动函数体。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
const STATES_MAP = {
state1: function () {},
state2: function () {},
state3: function () {},
state4: function () {},
}

function doAction(result) {
if (STATES_MAP[result]) {
STATES_MAP[result]()
}
}

// 然而,这时候states_map很容易从外部被修改,我们用闭包来统一封装下吧。
const States = (function () {
const STATES_MAP = {
state1: function () {},
state2: function () {},
state3: function () {},
state4: function () {},
}

function getStateFn(state) {
if (STATES_MAP[state]) {
return STATE_MAP[state]
}

throw Error(`${state}状态方法不存在`)
}

function add(state, fn) {
STATES_MAP[state] = fn
}

function remove(state) {
if (STATES_MAP[state]) {
STATES_MAP[state] = null
}
}

return {
getStateFn,
add,
remove
}
})()

// 增加状态
States.add('state5', function () {})
// 删除状态
States.remove('state4')

function doAction(result) {
let fn = States.getStateFn(result)
// todo
}

这就是状态模式,不同的state对应不同的action,这样一来,我们维护多状态的业务逻辑就方便多了。


下面来说 策略模式。

策略模式,就是采用多种策略解决一个问题。随着条件的改变,所用的策略不同。策略之间是可以相互替换的。

我们设想这样一个场景:假设一个电商平台,销售一台macpro。在双十一的时候会员价格7折,非会员在圣诞节的时候打8折出售,在元旦时8.5折出售。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const time_map = {
'11': 'price50',
'1225': 'price70',
'101': 'price85'
}

const STRATEGIES_MAP = {
price50(price) {
return price * 0.5
},

price70(price) {
return price * 0.7
},

price85(price) {
return price * 0.85
}
}

function sale(time, ori_price) {
const strategy = time_map[time]
const price = STRATEGIES_MAP[strategy] && STRATEGIES_MAP[strategy](ori_price)

// todo
}

// 当然,我们还可以像状态模式一样把策略封装起来,只暴露出特权方法。这里就不多讲啦。

也许这里可能大材小用了,不过这么做最起码有两个理由:

  1. 对于多个复杂的价格计算方式,这种策略模式是很实用的;
  2. 维护方便,便于复用;
到这里,我们能进一步理解策略模式区别于状态模式的地方了。
即策略模式为了解决一个问题,彼此之间可以替换;而状态模式用于处理不同状态下的行为,彼此之间不能替换

3. 设计模式之模板模式

模版模式,即提取同一类对象的共性封装成对象,通过继承实现各自功能。

在移动网页中,我们比较常见的是各种各样的toast。大概有这么多…

各种toast

要实现这些各种各样的toast,我们可以通过模板模式来实现。

首先我们通过观察各种toast,看看它们有什么相同点:
  1. 都有提示文字
  2. 都有透明的背景

除此之外,每个toast都有控制它显示隐藏的方法。大概就是这些了。

下面我们看看它们有什么不同点:
  1. 背景样式不一样
  2. 有些toast有图标
  3. 提示文案不一样

总结出以上结论后,我们有了基本的开发思路:

  1. 实现一个带有透明背景和文字的toast模板,并绑定show和hide方法
  2. 通过继承,实现个性化的toast
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
class Toast {
constructor(datas = {}) {
let {
text = '',
maskStyle = {},
duration = 1500
} = datas

this.timer = null
this.duration = duration

this.bgNode = document.createElement('div')
this.bgNode.className = 'toast-bg'

if (Object.keys(maskStyle)) {
this.setMarkStyle(maskStyle)
}

this.textNode = document.createElement('span')
this.textNode.className = 'toast-text'
this.textNode.innerText = text
}

init() {
this.bgNode.appendChild(this.textNode)
document.body.appendChild(this.bgNode)

return this
}

show() {
this.bgNode.style = 'block'

setTimeout(() => {
this.hide()
}, this.duration)
}

hide() {
this.bgNode.style = 'none'
}

setMaskStyle(styleObj) {
let styleArr = []

for (let key in styleObj) {
styleArr.push(`${key}=${styleObj[key]}`)
}

this.bgNode.style = styleArr.join(';')
}
}

// 设置一个loading的toast
class LoadingToast extends Toast {
constructor(datas) {
super(datas)

this.loadingIcon = data.loadingIcon

this.loadingNode = document.createElement('img')
this.loadingNode.className = 'toast-loading'
}

init() {
this.bgNode.insertBefore(this.loadingNode, this.bgNode.firstChild)
Toast.prototype.init.call(this)

return this
}
}

如上面所示,通过模板模式,我们实现了一个loadingToast。其它toast的实现策略类似。在这种模式下,代码能够实现最大程度的复用,也便于维护和扩展。

模板模式在日常开发中还有很多应用场景,当遇到类似的需求时,希望能够考虑以下模版模式哦~