小宋爱睡觉 小宋爱睡觉
首页
  • HTML
  • CSS
  • JavaScript
  • Vue
  • React
  • 计算机网络
  • 浏览器原理
  • 性能优化
  • 设计模式
手写系列
  • 字符串
  • 数组
  • 链表
  • 树
  • 动态规划
  • 排序算法
  • GitHub (opens new window)
  • JueJin (opens new window)
首页
  • HTML
  • CSS
  • JavaScript
  • Vue
  • React
  • 计算机网络
  • 浏览器原理
  • 性能优化
  • 设计模式
手写系列
  • 字符串
  • 数组
  • 链表
  • 树
  • 动态规划
  • 排序算法
  • GitHub (opens new window)
  • JueJin (opens new window)
  • 设计模式
    • 单例模式
    • 外观模式
    • 代理模式
    • 工厂模式
    • 策略模式
    • 观察者模式
    • 适配器模式
    • 发布订阅模式
    • 迭代器模式
    • 中介者模式
    • 访问者模式
  • 设计模式
Crucials
2022-01-10

设计模式

# 设计模式

# 单例模式重要

概念

顾名思义,单例模式中Class的实例个数最多为1。当需要一个对象去贯穿整个系统执行某些任务时,单例模式就派上了用场。而除此之外的场景尽量避免单例模式的使用,因为单例模式会引入全局状态,而一个健康的系统应该避免引入过多的全局状态

实现单例模式需要解决以下几个问题:

  • 如何确定Class只有一个实例?
  • 如何简便的访问Class的唯一实例?
  • Class如何控制实例化的过程?
  • 如何将Class的实例个数限制为1?

我们一般通过实现以下两点来解决上述问题:

  • 隐藏Class的构造函数,避免多次实例化
  • 通过暴露一个 getInstance() 方法来创建/获取唯一实例
class Singleton {
  constructor(name) {
    this.name = name;
    this.instance = null;
  }

  getName() {
    console.log(this.name);
  }

  getInstance(name) {
    if (!this.instance) {
      this.instance = new Singleton(name);
    }

    return this.instance;
  }
}

const singleton = new Singleton();

const a = singleton.getInstance("a");
const b = singleton.getInstance("b");

console.log(a);
console.log(b);
console.log(a === b);
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

实现的关键点有:

  • getInstance() 为一个 闭包 ,使用闭包保存局部作用域中的单例对象并返回。

场景例子

  • 定义命名空间和实现分支型方法
  • 登录框
  • vuex 和 redux中的store
  • 适用于弹框的实现, 全局缓存,一个单一对象。比如:弹窗,无论点击多少次,弹窗只应该被创建一次

优点

  • 划分命名空间,减少全局变量
  • 增强模块性,把自己的代码组织在一个全局变量名下,放在单一位置,便于维护
  • 且只会实例化一次。简化了代码的调试和维护

缺点

  • 由于单例模式提供的是一种单点访问,所以它有可能导致模块间的强耦合,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
  • 从而不利于单元测试。无法单独测试一个调用了来自单例的方法的类,而只能把它与那个单例作为一 个单元一起测试。

# 外观模式

概念

外观模式是最常见的设计模式之一,它为子系统中的一组接口提供一个统一的高层接口,使子系统更容易使用。简而言之外观设计模式就是把多个子系统中复杂逻辑进行抽象,从而提供一个更统一、更简洁、更易用的API。比如JQuery就把复杂的原生DOM操作进行了抽象和封装,并消除了浏览器之间的兼容问题,从而提供了一个更高级更易用的版本。

let addMyEvent = function (el, ev, fn) {
    if (el.addEventListener) {
        el.addEventListener(ev, fn, false)
    } else if (el.attachEvent) {
        el.attachEvent('on' + ev, fn)
    } else {
        el['on' + ev] = fn
    }
};
1
2
3
4
5
6
7
8
9

封装接口

let myEvent = {
    // ...
    stop: e => {
        e.stopPropagation();
        e.preventDefault();
    }
};
1
2
3
4
5
6
7

场景

  • 设计初期,应该要有意识地将不同的两个层分离,比如经典的三层结构,在数据访问层和业务逻辑层、业务逻辑层和表示层之间建立外观Facade
  • 在开发阶段,子系统往往因为不断的重构演化而变得越来越复杂,增加外观Facade可以提供一个简单的接口,减少他们之间的依赖。
  • 在维护一个遗留的大型系统时,可能这个系统已经很难维护了,这时候使用外观Facade也是非常合适的,为系系统开发一个外观Facade类,为设计粗糙和高度复杂的遗留代码提供比较清晰的接口,让新系统和Facade对象交互,Facade与遗留代码交互所有的复杂工作。

优点

  • 减少系统相互依赖
  • 提高灵活性
  • 提高了安全性

缺点

不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。

# 代理模式重要

概念

为某个对象提供一种代理以控制对这个对象的访问(自定义方法,可以使用这个对象的资源)

举个 🌰:双十一,小美有亿件快递到了,有些包裹太重了自己拿不动。于是,她拜托工具人小明帮忙,小明欣然前往快递点取件。这里,小明帮小美取快递就起到了代理的作用。注意:整个动作还是小美发起的,小明可以理解为一个透明的中间人,直接看代码。

let expressPoint = {
    pickUp() {
        console.log('取快递成功...')
    }
}
let Ming = {
    getMsg(target) {
        target.pickUp()
    }
}
let Mei = {
    getExpress(target) {
        Ming.getMsg(target) // 小明取件,可以配合定时器等逻辑做到延迟取件
    }
}
Mei.getExpress(expressPoint) // 小美取件,虽然是通过小明代理的。

// 取快递成功...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

场景

HTML元 素事件代理

<ul id="ul">
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>
<script>
  let ul = document.querySelector('#ul');
  ul.addEventListener('click', event => {
    console.log(event.target);
  });
</script>
1
2
3
4
5
6
7
8
9
10
11

优点

  • 代理模式能将代理对象与被调用对象分离,降低了系统的耦合度。代理模式在客户端和目标对象之间起到一个中介作用,这样可以起到保护目标对象的作用
  • 代理对象可以扩展目标对象的功能;通过修改代理对象就可以了,符合开闭原则;

缺点

处理请求速度可能有差别,非直接访问存在开销

# 工厂模式重要

概念

工厂模式定义一个用于创建对象的接口,这个接口由子类决定实例化哪一个类。该模式使一个类的实例化延迟到了子类。而子类可以重写接口方法以便创建的时候指定自己的对象类型。

class Product {
    constructor(name) {
        this.name = name
    }
    init() {
        console.log('init')
    }
    fun() {
        console.log('fun')
    }
}

class Factory {
    create(name) {
        return new Product(name)
    }
}

// use
let factory = new Factory()
let p = factory.create('p1')
p.init()
p.fun()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

场景

  • 如果你不想让某个子系统与较大的那个对象之间形成强耦合,而是想运行时从许多子系统中进行挑选的话,那么工厂模式是一个理想的选择
  • 将new操作简单封装,遇到new的时候就应该考虑是否用工厂模式;
  • 需要依赖具体环境创建不同实例,这些实例都有相同的行为,这时候我们可以使用工厂模式,简化实现的过程,同时也可以减少每种对象所需的代码量,有利于消除对象间的耦合,提供更大的灵活性

优点

  • 创建对象的过程可能很复杂,但我们只需要关心创建结果。
  • 构造函数和创建者分离, 符合“开闭原则”
  • 一个调用者想创建一个对象,只要知道其名称就可以了。
  • 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。

缺点

  • 添加新产品时,需要编写新的具体产品类,一定程度上增加了系统的复杂度
  • 考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度

什么时候不用

当被应用到错误的问题类型上时,这一模式会给应用程序引入大量不必要的复杂性.除非为创建对象提供一个接口是我们编写的库或者框架的一个设计上目标,否则我会建议使用明确的构造器,以避免不必要的开销。

由于对象的创建过程被高效的抽象在一个接口后面的事实,这也会给依赖于这个过程可能会有多复杂的单元测试带来问题。

# 策略模式重要

概念

策略模式简单描述就是:对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。把它们一个个封装起来,并且使它们可以互相替换

<html>
<head>
    <title>策略模式-校验表单</title>
    <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
</head>
<body>
    <form id = "registerForm" method="post" action="http://xxxx.com/api/register">
        用户名:<input type="text" name="userName">
        密码:<input type="text" name="password">
        手机号码:<input type="text" name="phoneNumber">
        <button type="submit">提交</button>
    </form>
    <script type="text/javascript">
        // 策略对象
        const strategies = {
          isNoEmpty: function (value, errorMsg) {
            if (value === '') {
              return errorMsg;
            }
          },
          isNoSpace: function (value, errorMsg) {
            if (value.trim() === '') {
              return errorMsg;
            }
          },
          minLength: function (value, length, errorMsg) {
            if (value.trim().length < length) {
              return errorMsg;
            }
          },
          maxLength: function (value, length, errorMsg) {
            if (value.length > length) {
              return errorMsg;
            }
          },
          isMobile: function (value, errorMsg) {
            if (!/^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|17[7]|18[0|1|2|3|5|6|7|8|9])\d{8}$/.test(value)) {
              return errorMsg;
            }                
          }
        }
        
        // 验证类
        class Validator {
          constructor() {
            this.cache = []
          }
          add(dom, rules) {
            for(let i = 0, rule; rule = rules[i++];) {
              let strategyAry = rule.strategy.split(':')
              let errorMsg = rule.errorMsg
              this.cache.push(() => {
                let strategy = strategyAry.shift()
                strategyAry.unshift(dom.value)
                strategyAry.push(errorMsg)
                return strategies[strategy].apply(dom, strategyAry)
              })
            }
          }
          start() {
            for(let i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
              let errorMsg = validatorFunc()
              if (errorMsg) {
                return errorMsg
              }
            }
          }
        }

        // 调用代码
        let registerForm = document.getElementById('registerForm')

        let validataFunc = function() {
          let validator = new Validator()
          validator.add(registerForm.userName, [{
            strategy: 'isNoEmpty',
            errorMsg: '用户名不可为空'
          }, {
            strategy: 'isNoSpace',
            errorMsg: '不允许以空白字符命名'
          }, {
            strategy: 'minLength:2',
            errorMsg: '用户名长度不能小于2位'
          }])
          validator.add(registerForm.password, [ {
            strategy: 'minLength:6',
            errorMsg: '密码长度不能小于6位'
          }])
          validator.add(registerForm.phoneNumber, [{
            strategy: 'isMobile',
            errorMsg: '请输入正确的手机号码格式'
          }])
          return validator.start()
        }

        registerForm.onsubmit = function() {
          let errorMsg = validataFunc()
          if (errorMsg) {
            alert(errorMsg)
            return false
          }
        }
    </script>
</body>
</html>
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105

场景例子

  • 如果在一个系统里面有许多类,它们之间的区别仅在于它们的'行为',那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
  • 一个系统需要动态地在几种算法中选择一种。
  • 表单验证

优点

  • 利用组合、委托、多态等技术和思想,可以有效的避免多重条件选择语句
  • 提供了对开放-封闭原则的完美支持,将算法封装在独立的strategy中,使得它们易于切换,理解,易于扩展
  • 利用组合和委托来让Context拥有执行算法的能力,这也是继承的一种更轻便的代替方案

缺点

  • 会在程序中增加许多策略类或者策略对象
  • 要使用策略模式,必须了解所有的strategy,必须了解各个strategy之间的不同点,这样才能选择一个合适的strategy

# 观察者模式重要

概念

日常生活中的应用也比比皆是,比如你订阅了某个博主的频道,当有内容更新时会收到推送;又比如JavaScript中的事件订阅响应机制。观察者模式的思想用一句话描述就是:被观察对象(subject)维护一组观察者(observer),当被观察对象状态改变时,通过调用观察者的某个方法将这些变化通知到观察者

class Subject {
  constructor() {
    this.observers = []; // 观察者队列
  }
  add(observer) {
    // 没有事件通道
    this.observers.push(observer); // 必须将自己 observer 添加到观察者队列
    this.observers = [...new Set(this.observers)];
  }
  notify(...args) {
    // 亲自通知观察者
    this.observers.forEach((observer) => observer.update(...args));
  }
  remove(observerToRemove) {
        this.observers = this.observers.filter(observer => {
      return observer !== observerToRemove;
    })
  }
}

class Observer {
  update(...args) {
    console.log(...args);
  }
}

let observer_1 = new Observer(); // 创建观察者1
let observer_2 = new Observer();
let sub = new Subject(); // 创建目标对象
sub.add(observer_1); // 添加观察者1
sub.add(observer_2);
sub.notify("I changed !");
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

场景

  • DOM事件
document.body.addEventListener('click', function() {
    console.log('hello world!');
});
document.body.click()
1
2
3
4
  • vue 响应式

优点

  • 支持简单的广播通信,自动通知所有已经订阅过的对象
  • 目标对象与观察者之间的抽象耦合关系能单独扩展以及重用
  • 增加了灵活性
  • 观察者模式所做的工作就是在解耦,让耦合的双方都依赖于抽象,而不是依赖于具体。从而使得各自的变化都不会影响到另一边的变化。

缺点

过度使用会导致对象与对象之间的联系弱化,会导致程序难以跟踪维护和理解

# 适配器模式重要

适配器模式 的作用就是解决两个软件实体间接口不兼容情况,实体电器例如电源适配器、USB 转接口、各种转换器等。

需求来了,现在需要 对接多个快递平台 SDK 进行不同快递单的生成 功能。

// 顺丰
const sfOrderService = {
    create() {
    	console.log('顺丰订单已生成...')
    }
}
// 韵达
const ydOrderService = {
    create() {
    	console.log('韵达订单已生成...')
    }
}
// createOrder 提供给使用者调用
const createOrder = (express) => {
    if (express.create instanceof Function) express.create()
}
createOrder(sfOrderService)
createOrder(ydOrderService)

// 顺丰订单已生成...
// 韵达订单已生成...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

现在新的需求来了,我们需要集成圆通 SDK,但是圆通 SDK 的方法是 generate,不是 create。为了满足开闭原则,我们想到了适配器模式。

// 圆通
const ytOrderService = {
    generate() {
    	console.log('圆通订单已生成...')
    }
}
// 适配器
const ytExpressAdapater = {
    create() {
    	return ytOrderService.generate()
    }
}
// 现在可以使用 createOrder 生成订单了,哈哈
createOrder(ytExpressAdapater)

// 圆通订单已生成...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

另外一个常见的开发场景:数据格式变更

// 这是我们之前上传资源,后台给我们返回的文件信息
const responseUploadFile = {
    startTime: '',
    file: {
    	size: '100kb',
        type: 'text',
        ...
    },
    id: ''
}
// 某天后台将返回格式变动了,变为:
const changeResUploadFile = {
    size: '100kb',
    type: 'text',
    startTime: '',
    id: '',
    ...
}
// 为了不影响旧有业务,导致 BUG 和回归测试,写个适配器用来数据转换吧...
const responseUploadFileAdapter = (uploadFile) = > {
    const { startTime = '', size = '', type = '', id = '', ... } = uploadFile
    return {
    	startTime,
        file: {
            size,
            type,
            ...
        },
        id
    }
}
responseUploadFileAdapter(changeResUploadFile) // 转换成旧格式了
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

由此看出前后端分离开发,数据操作的 自由度 是很高的。

# 发布订阅模式重要

前面已经梳理过区别了,直接开始实现:

class PublicSubject { // 只有一个调度中心
    constructor() {
        this.subscribers = {}
    }
    subscribe(type, callback) { // 订阅
        let res = this.subscribers[type]
        if (!res) {
            this.subscribers[type] = [callback]
        } else {
            res.push(callback)
        }
    }
    publish(type, ...args) { // 发布
        let res = this.subscribers[type] || []
        res.forEach(callback => callback(...args))
    }
}

let pubSub = new PublicSubject()
pubSub.subscribe('blog', (arg) => console.log(`${arg} 更新了`)) // A 订阅 Keith
pubSub.subscribe('blog', (arg) => console.log(`${arg} 更新了`)) // B 订阅 Keith
pubSub.publish('blog', '掘金 Keith')

// 掘金 Keith 更新了
// 掘金 Keith 更新了
复制代码
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

当然,这个版本功能还不够完善,实际上,发布-订阅模式 通常被用在事件监听和触发功能上,我们可能还需求移除订阅。比如常见的 Vue 父子组件通信 $emit、$on、$off、$once,Vue 的响应式,后文要介绍的 redux,以及 nodejs Event 模块 的 eventEmitter 类实现等均有应用。

那么,我们来实现下:

// once 参数表示是否只是触发一次
const wrapCallback = (fn, once=false) => ({ callback: fn, once })
class EventEmitter {
    constructor() {
        this.events = new Map()
    }
    on(type, fn, once=false) { // 监听订阅
        let handler = this.events.get(type)
        if (!handler) {
            this.events.set(type, wrapCallback(fn, once)) // 绑定回调
        } else if (handler && typeof handler.callback === 'function') {
            this.events.set(type, [handler, wrapCallback(fn, once)]) // 超过一个转为数组
        } else {
            handler.push(wrapCallback(fn, once))
        }
    }
    off(type, fn) { // 删除某个事件的回调,假如回调 <= 1,则等同 allOff 方法
        let handler = this.events.get(type)
        if (!handler) return;
        // 只有一个回调事件直接删除该订阅
        if (!Array.isArray(handler) && 
        handler.callback === fn.callback) this.events.delete(type)
        for (let i=0; i<handler.length; i++) {
            let item = handler[i]
            if (item.callback === fn.callback) {
                handler.splice(i, 1)
                i-- // 数组塌陷,i 往前一位
                if (handler.length === 1) this.events.set(type, handler[0])
            }
        }
    }
    // once:该订阅事件 type 只触发一次,之后自动移除
    once(type, fn) {
        this.on(type, fn, true)
    }
    emit(type, ...args) {
        let handler = this.events.get(type)
        if (!handler) return;
        if (Array.isArray(handler)) {
            handler.map(item => {
                item.callback.apply(this, args) // args 参数少,可以换成 call
                if (item.once) this.off(type, item) // 处理 once 的情况,off 移除
            })
        } else {
            handler.callback.apply(this, args) // 处理非数组
        }
    }
    allOff(type) {
        let handler = this.events.get(type)
        if (!handler) return;
        this.events.delete(type)
    }
}

let e = new EventEmitter()
e.on('eventA', () => {
  console.log('eventA 事件触发')
})
e.on('eventA', () => {
  console.log('✨ eventA 事件又触发了 ✨')
})

function f() { 
  console.log('eventA 事件我只触发一次');
}
e.once('type', f)
e.emit('type')
e.emit('type')
e.allOff('type')
e.emit('type')

// eventA 事件触发
// ✨ eventA 事件又触发了 ✨
// eventA 事件我只触发一次
// eventA 事件触发
// ✨ eventA 事件又触发了 ✨
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
72
73
74
75
76

# 迭代器模式

概念

迭代器模式简单的说就是提供一种方法顺序一个聚合对象中各个元素,而又不暴露该对象的内部表示。

迭代器模式解决了以下问题:

  • 提供一致的遍历各种数据结构的方式,而不用了解数据的内部结构
  • 提供遍历容器(集合)的能力而无需改变容器的接口

一个迭代器通常需要实现以下接口:

  • hasNext():判断迭代是否结束,返回Boolean
  • next():查找并返回下一个元素

下面为Javascript的数组实现一个迭代器可以这么写:

const item = [1, 'red', false, 3.14];

function Iterator(items) {
  this.items = items;
  this.index = 0;
}

Iterator.prototype = {
  hasNext: function () {
    return this.index < this.items.length;
  },
  next: function () {
    return this.items[this.index++];
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

验证一下迭代器是否工作:

const iterator = new Iterator(item);

while(iterator.hasNext()){
  console.log(iterator.next());
}
//输出:1, red, false, 3.14
1
2
3
4
5
6

ES6提供了更简单的迭代循环语法 for...of,使用该语法的前提是操作对象需要实现 可迭代协议(The iterable protocol),简单说就是该对象有个Key为 Symbol.iterator 的方法,该方法返回一个iterator对象。

比如我们实现一个 Range 类用于在某个数字区间进行迭代:

function Range(start, end) {
  return {
    [Symbol.iterator]: function () {
      return {
        next() {
          if (start < end) {
            return { value: start++, done: false };
          }
          return { done: true, value: end };
        }
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

验证一下:

for (num of Range(1, 5)) {
  console.log(num);
}
// 输出:1, 2, 3, 4
1
2
3
4

# 中介者模式

  • 在中介者模式中,中介者(Mediator)包装了一系列对象相互作用的方式,使得这些对象不必直接相互作用,而是由中介者协调它们之间的交互,从而使它们可以松散偶合。当某些对象之间的作用发生改变时,不会立即影响其他的一些对象之间的作用,保证这些作用可以彼此独立的变化。
  • 中介者模式和观察者模式有一定的相似性,都是一对多的关系,也都是集中式通信,不同的是中介者模式是处理同级对象之间的交互,而观察者模式是处理Observer和Subject之间的交互。中介者模式有些像婚恋中介,相亲对象刚开始并不能直接交流,而是要通过中介去筛选匹配再决定谁和谁见面。

场景

例如购物车需求,存在商品选择表单、颜色选择表单、购买数量表单等等,都会触发change事件,那么可以通过中介者来转发处理这些事件,实现各个事件间的解耦,仅仅维护中介者对象即可。

var goods = {   //手机库存
    'red|32G': 3,
    'red|64G': 1,
    'blue|32G': 7,
    'blue|32G': 6,
};
//中介者
var mediator = (function() {
    var colorSelect = document.getElementById('colorSelect');
    var memorySelect = document.getElementById('memorySelect');
    var numSelect = document.getElementById('numSelect');
    return {
        changed: function(obj) {
            switch(obj){
                case colorSelect:
                    //TODO
                    break;
                case memorySelect:
                    //TODO
                    break;
                case numSelect:
                    //TODO
                    break;
            }
        }
    }
})();
colorSelect.onchange = function() {
    mediator.changed(this);
};
memorySelect.onchange = function() {
    mediator.changed(this);
};
numSelect.onchange = function() {
    mediator.changed(this);
};
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
  • 聊天室里

聊天室成员类:

function Member(name) {
  this.name = name;
  this.chatroom = null;
}

Member.prototype = {
  // 发送消息
  send: function (message, toMember) {
    this.chatroom.send(message, this, toMember);
  },
  // 接收消息
  receive: function (message, fromMember) {
    console.log(`${fromMember.name} to ${this.name}: ${message}`);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

聊天室类:

function Chatroom() {
  this.members = {};
}

Chatroom.prototype = {
  // 增加成员
  addMember: function (member) {
    this.members[member.name] = member;
    member.chatroom = this;
  },
  // 发送消息
  send: function (message, fromMember, toMember) {
    toMember.receive(message, fromMember);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

测试一下:

const chatroom = new Chatroom();
const bruce = new Member('bruce');
const frank = new Member('frank');

chatroom.addMember(bruce);
chatroom.addMember(frank);

bruce.send('Hey frank', frank);

//输出:bruce to frank: hello frank
1
2
3
4
5
6
7
8
9
10

优点

  • 使各对象之间耦合松散,而且可以独立地改变它们之间的交互
  • 中介者和对象一对多的关系取代了对象之间的网状多对多的关系
  • 如果对象之间的复杂耦合度导致维护很困难,而且耦合度随项目变化增速很快,就需要中介者重构代码

缺点

系统中会新增一个中介者对象,因为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的。中介 者对象自身往往就是一个难以维护的对象。

# 访问者模式

概念

访问者模式 是一种将算法与对象结构分离的设计模式,通俗点讲就是:访问者模式让我们能够在不改变一个对象结构的前提下能够给该对象增加新的逻辑,新增的逻辑保存在一个独立的访问者对象中。访问者模式常用于拓展一些第三方的库和工具。

// 访问者  
class Visitor {
    constructor() {}
    visitConcreteElement(ConcreteElement) {
        ConcreteElement.operation()
    }
}
// 元素类  
class ConcreteElement{
    constructor() {
    }
    operation() {
       console.log("ConcreteElement.operation invoked");  
    }
    accept(visitor) {
        visitor.visitConcreteElement(this)
    }
}
// client
let visitor = new Visitor()
let element = new ConcreteElement()
elementA.accept(visitor)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

访问者模式的实现有以下几个要素:

  • Visitor Object:访问者对象,拥有一个visit()方法
  • Receiving Object:接收对象,拥有一个 accept() 方法
  • visit(receivingObj):用于Visitor接收一个Receiving Object
  • accept(visitor):用于Receving Object接收一个Visitor,并通过调用Visitor的 visit() 为其提供获取Receiving Object数据的能力

简单的代码实现如下:

Receiving Object:

function Employee(name, salary) {
  this.name = name;
  this.salary = salary;
}

Employee.prototype = {
  getSalary: function () {
    return this.salary;
  },
  setSalary: function (salary) {
    this.salary = salary;
  },
  accept: function (visitor) {
    visitor.visit(this);
  }
}
Visitor Object:

function Visitor() { }

Visitor.prototype = {
  visit: function (employee) {
    employee.setSalary(employee.getSalary() * 2);
  }
}
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

验证一下:

const employee = new Employee('bruce', 1000);
const visitor = new Visitor();
employee.accept(visitor);

console.log(employee.getSalary());//输出:2000
1
2
3
4
5

场景

对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作

需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,也不希望在增加新操作时修改这些类。

优点

  • 符合单一职责原则
  • 优秀的扩展性
  • 灵活性

缺点

  • 具体元素对访问者公布细节,违反了迪米特原则
  • 违反了依赖倒置原则,依赖了具体类,没有依赖抽象。
  • 具体元素变更比较困难
上次更新: 2022/03/20, 19:40:28
Copyright © 2021-2025 粤ICP备2021165371号
  • 跟随系统
  • 浅色模式
  • 深色模式