发布-订阅模式
其实这个模式使用频率很多,之前楼主写react的时候,就遇到过这么个场景,一个页面中涉及的组件有:
- form part(一堆筛选控件,如一级二级select,datepicker,地理位置选择器等等)
- slider嵌套的多个card,card内部又有统计数据部分和echarts图表部分,随着formpart的变动而更改。
card中的数据变动不定,而且里面有很多子组件,最开始的时候通信是用props一层层的传递下去,这样遇到两个问题:
- 组件嵌套过深时,通过props传递时,中间组件由于中介作用需要传递很多中间数据
- 从grandparent传递下来,使得很多实际并没有变化的card也进行了渲染。(嗯,可以用should component update检测,这是后话)。
中途进行了一个重构就是引入了event-bus.每个card 注册监听事件,监听ajax的success中数据的返回(包括应该变更的card ID),如果是自己该更新,那么就render。而那些无需变动的card选择忽略就好。
后来写的vue项目中,(1.0版本的时候还有$broadcast
跟$dispatch
等等),2.0版本中只有$emit
,在父组件中注册监听事件,(有点点像只有一层的props传递函数)。
除此之外,还有watch方法,computed计算属性等等,都是发布-订阅模式的一个实现。
实现
|
|
取消订阅的话,只需要在fnsArr里面splice即可。
这边的observer是function。
其他的case有很多,上面我们已经说过了,书中提到的例子有网站登录
上面的方法还有一个小问题是,在observer代码里,需要知晓它监听的对象,即login。那么有一种方法是建立一个全局的事件对象,类似于中介者,去进行事件的统筹分配。订阅者不需要了解消息来自哪个发布者,发布者也不知道消息会推送给哪些订阅者,Event作为一个类似“中介者”的角色,把订阅者和发布者联系起来。
|
|
全局event对象的一个不足之处是会造成命名冲突,因此可以添加namespace,具体代码见书。
后话是,楼主用event重构的react的项目后来又换回去了一部分,因为一是性能问题可以用shouldcomponentupdate来解决,另一方面,大量的event不好追踪一个事件的双方,那么模块与模块之间的联系就被隐藏到了背后。我们最终会搞不清楚消息来自哪个模块,或者消息会流向哪些模块,感觉一个代码里充斥着越过组件的攻击曲线..=.=
作用
问: 跟加了回调的代理模式有啥区别?
- 可以不再显式调用某一个函数,达到松散耦合,比如说,之前在登录成功的回调里面处理header,nav的信息,当模块多了的话会要不停修改login成功回调,现在不需要动登陆成功的回调函数,只要各自监听就好,可以不断添加observer。
- 而且,局部订阅者模式是在某一方加了接口,而代理是在两方基础上添加一个中间处理方:一种是保护,另一种是多做一些性能的额外处理,比如合并请求,预加载,懒加载等等。那中介者模式呢…
其他
- 符合“好莱坞原则”
- 离线消息的存在必要性: 在很多情况下,并不是严格按照订阅-发布的时间顺序,比如ajax回调中会trigger一个事件,但是trigger事件的时候,可能没有订阅者,或者订阅者还没有准备好,那么他们就无法捕获到这个事件的发生,就会遗漏事件,因此有必要将暂时还没有接受者的事件存储下来,当出现订阅者时发送给他们,执行一次即可,阅后即焚…
中介者模式
当对象变多时,他们之间的关系错综复杂,中介者模式就是为了解除对象之间的紧密耦合关系而存在的,应用以后对象之间都通过中介者通信,互相不透镜,使得多对多关系变为比较简单的一对多关系。
其实之前在观察者模式中我们也涉及到了中介者模式,就是最后的全局Event对象,它对对象之间的注册触发进行了统筹。
既然放一起了,就做个区分吧:
观察者模式最朴素的是A,B之间进行处理,A需要知道B的存在,就是多对多的关系。
改进一点的方法是用中间的全局Event对象去进行管理。仔细想一下的话,中介者模式是为了观察者模式做服务的,它只是用来处理双方之间的关系,而观察者模式是用来解决消息通知问题的,中介者是为了观察者模式更好的进行而存在的。
盗图一张…
书中的一个例子是玩游戏,设用户为A,B,C…添加一个中间人,每次A,B等有操作时通知中间人。
|
|
上面的代码是对象通知中介者,那中介者收到这些消息后进行处理,会将信息发送给各对象,这边可以遍历,调用对象的方法。
|
|
而关于这个中介者的实现上,书中提到了两种方法:
>
利用发布—订阅模式。将playerDirector实现为订阅者,各player作为发布者,一旦player的状态发生改变,便推送消息给playerDirector,playerDirector处理消息后将反馈发送给其他player。
在playerDirector中开放一些接收消息的接口,各player可以直接调用该接口来给playerDirector发送消息,player只需传递一个参数给playerDirector,这个参数(书中为 this ,即当前对象)的目的是使playerDirector可以识别发送者。同样,playerDirector接收到消息之后会将处理结果反馈给其他player。
中介者模式是迎合迪米特法则的一种实现。迪米特法则也叫最少知识原则,是指一个对象应该尽可能少地了解另外的对象(类似不和陌生人说话)。
【summary】中介者模式总结来说,是将耦合转嫁到一个中间人去管理这些,类似于“上帝之手”,解决的是对象之间的关系。
装饰器模式
在程序开发中,许多时候都并不希望某个类天生就非常庞大,一次性包含许多职责。那么我们就可以使用装饰者模式,不停的包装对象,生成一个又一个嵌套的对象,每次都增加一丢丢功能,类似于俄罗斯套娃。
装饰者模式可以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。
这种功能通常可以用继承来做,但是存在一点问题,就是:
- 会造成父类和子类的强耦合,父类改变时,子类会随之变动
- 当功能增多是,会出现大量的子类
而装饰器模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。跟继承相比,装饰者是一种更轻便灵活的做法。
JavaScript天然可以增加属性/职责,如下:obj.address = obj.address +'福田区'
示例
|
|
这种方法最简单粗暴,直接改写了原有方法(覆盖),增加了中间变量_fire(一定要增加…否则就是递归调用了=。=)。
而且有时候会遇到this被劫持的问题,比如document.getElementById中的this.
正确的写法如下
|
|
还有一个validate的部分,在策略模式那边添加了部分代码,可以走过路过看一下:)
装饰器模式与代理模式
- 代理模式通常只有一层代理-本体的引用,而装饰者模式经常会形成一条长长的装饰链。
- 装饰器模式与代理模式都是为本体添加功能,但是前者是真正添加额外功能,而后者是将原有功能优化,如性能等
适配器模式
这种模式相对简单,主要是针对接口的兼容修补工作,使得A,B两个对外呈现的接口一致。
|
|
####区分
装饰器模式会形成长长的包装链、代理和适配器只包装一次。
代理模式是不变功能,只是功能更加聪明,性能更好,装饰器是增加功能,适配器模式是改外部接口,不涉及内部功能。
享元模式
常用于性能优化,如果项目中有大量的相似对象,可以把他们的状态抽取成可以共用的内部状态和外部的场景状态。
如书中介绍到的内衣工厂,model对象的属性为性别,整个项目中设置两个对象,男女模特,然后衣服设置为外部状态。
|
|
通常来说,内部状态有多少种组合,系统中就有多少个对象。
另一个case就是文件上传(敲黑板,之前文件上传的时候我们用过迭代器模式)
可以看到之前是每个文件对象里面包括了这么多属性
那么用享元模式的话要把这些属性分类,抽取出“上传类型”这个内部属性,其他的都归结为外部属性
这边借助闭包实现一个共享的对象(也可以认为是一个单例模式的粗陋实现)
|
|
除此之外,需要注意的是,享元状态可以只有外部状态(内部状态只有一个),这个时候实际上就是一个单例模式。
另,还有一种不区分内外状态的对象池,可以用于DOM节点的回收和创建。
状态模式
之前我们说过利用鸭子类型来避免if-else的反复code,是将接口/函数的不同转变为对象的多态性,这边要介绍的状态模式则是把状态封装成类,状态中的同一接口函数定义了状态的切换。
每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部,每次状态变化时把请求委托给当前的状态对象即可,该状态对象会负责渲染它自身的行为。
调用的时候直接是
|
|
也就是说每个状态类需要实现同样的接口,由于跟模板方式中我们介绍过的一样,JavaScript直到执行时才会报错,(throw error)。
其实想一下,Vue或者react中就有状态模式的概念,react中直接就是state对象,每次都去setstate切换状态,去重新渲染,切换状态后又会有新的行为,就是这个状态下的操作。
Vue中对应的大概就是data对象了,只不过这个状态模式我们感受的不太明显,地表以上对开发人员透明的感觉。
状态模式与策略模式
- 策略模式中的各个策略类之间是平等又平行的,它们之间没有任何联系,之间的切换是由用户手动操作的。
- 状态模式中,状态和状态对应的行为是早已被封装好的,状态之间的切换也早被规定完成,被封装在了状态类内部,对客户来说,并不需要了解这些细节。这正是状态模式的作用所在
其他
设计原则:
- 单一职责原则:代理模式、装饰器模式、迭代器模式(回调函数)、单例模式(通用的创建移到外部)
- 最少知识原则(迪米特法则):应当尽量减少对象之间的交互。常见的做法是增加一个中间人,如中介者模式 ,去管理他们之间的关系。
- 外观模式
- 中介者模式
- 作用域也是广义的最少知识原则的体现,限定在一个小区域内known
- 开放封闭原则: 可扩展,但不允许修改
- 对象的多态性代替分支
- 在变化的地方放置hook,回调
- 设计模式中的实践:
- 发布-订阅模式(增加订阅者不需要改动发布者内部函数)
- 模板方法模式:(增加子类)
- 策略模式
- 代理模式(本体实现基本的功能不动就好)
- 职责链模式
前辈总结的这些设计原则通常指的是单一职责原则、里氏替换原则、依赖倒置原则、接口隔离原则、合成复用原则和最少知识原则。
代码编写时的建议:
- 提炼函数
- 合并重复的条件片段
- 分支条件语句提炼成函数便于调试
- 合理使用循环(迭代器模式)代替if-else
- 提前让函数return (如果有多个if-else的haul)
- 使用object作为参数来代替多个参数(因为会有顺序的问题,当然es6的话还可以解构)
- 尽量减少参数数量
- 多条件嵌套时少用三目运算符
- 分解大型类
to distinguish
- [ ] 发布-订阅者模式、中介者模式
- [ ] 装饰器模式、适配器模式、代理模式
- [ ] 享元模式、状态模式
- [ ] 模板方法模式与装饰器模式