使用观察者模式(发布-订阅模式)对抽象组件与业务组件进行解耦

Posted by Wanxiang Long(Ryuurock) on 2017-09-04

最近在重构一处功能,发现抽象组件居然依赖了业务组件的代码,从window作用域call了业务组件提供的方法,这里的强耦合让我强迫症又犯了,本着每天进步一点点的原则,毅然决然的进行解耦合。

我们在学习面向对象的时候,有一条设计原则叫依赖倒置原则

  • A.高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。
  • B.抽象不应该依赖于具体实现,具体实现应该依赖于抽象。

其实用通俗点的话来讲,就是通用模块应该高冷,不应该为了某个页面的需求变动或某项功能的改变就要我专门对你进行适配,写一些偏业务的代码,比如一个电脑操作系统更新,兼容性问题一般不会由操作系统厂商来解决,都是软件开发商来适配更新。放到代码上也是同理,因为你不知道有多少业务模块回来使用你,所以要保持必要的抽象性。

简单描述下我遇到的问题,组件A其实是为页面B服务的,但是大多数页面都需要通过组件A的功能进行操作,跳转到页面B,然后组件A再将刚才的操作告诉B以进行处理。所以这里之前我就写了依赖于具体的代码。往往很残酷的是遇到需求变更,这里处理可能要移动到C页面,或是增加D和E页面都要处理,于是就很尴尬了,难道每增加一个页面我就得去动A组件?于是我想到了观察者模式(很多又称发布-订阅模式)

  1. 组件A与页面B撇清关系
  2. 组件A来发布一个消息,我管你接收不接收?反正我正常发出去了
  3. 页面B甚至C、D、E、F…来订阅这个消息以进行处理
  4. 那么这样就完成了组件与具体实现的解耦
    下面是实现的代码

A.js 组件A

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

// 用于存放订阅者的队列
const _MESSAGES = {};

/**
* 向外提供一个用于订阅事件的函数
* @param type 订阅消息的类型
* @param fn 消息发生时的回调函数
*/
export function register( type, fn ) {
_MESSAGES[ type ] ? _MESSAGES[ type ].push( fn ) : _MESSAGES[ type ] = [ fn ];
}
/**
* 再提供提供一个用于发布事件的函数,组件可用,页面也可以用
* @param type 发布事件的类型
* @param backParam 发布事件时携带的参数
*/
export function trigger( type, ...backParam ) {
return _MESSAGES[ type ] && _MESSAGES[ type ].forEach( item => item.apply( this, backParam ) );
}

// 然后是组件本身的一些操作做完后
// some code
// 发布一个叫message的事件,并回传一个叫'hello world'的字符串
trigger( 'message', 'hello world!' );

B.js 页面B,依赖组件A

1
2
3
4
5
6
7
// 引入
import { register } from 'A';

// 组件A发布叫message的事件
register( 'message', function( data ) {
// 在回调里处理我们的事情
} );

这样我们就完成了一个简单的订阅-发布模式,当然我们还可以一步步对其进行完善,比如我们要取消订阅,增加一个叫remove的函数用于取消对抽象组件的消息订阅。

其实观察者模式在我们代码中无处不在,比如我们在多处对某个dom元素进行事件绑定,事件发生后会按照订阅顺序对订阅者进行分发。