设计模式之--“中介者模式” 和 “装饰者模式”

1.中介者模式

面向对象设计鼓励将行为分布到各个对象中,把对象划分成更小的粒度,有助于增强对象的可复用性,但由于这些细粒度对象之间的联系激增,又有可能会反过来降低它们的可复用性

中介者模式的作用就是解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。中介者模式使网状的多对多关系变成了相对简单的一对多关系

中介者模式是迎合迪米特法则的一种实现。迪米特法则也叫最少知识原则,是指一个对象应该尽可能少地了解另外的对象。如果对象之间的耦合性太高,一个对象发生改变之后,难免会影响到其他的对象。而在中介者模式里,对象之间几乎不知道彼此的存在,它们只能通过中介者对象来互相影响对方

因此,中介者模式使各个对象之间得以解耦,以中介者和对象之间的一对多关系取代了对象之间的网状多对多关系。各个对象只需要关注自身功能的实现,对象之间的交互关系交给了中介者对象来实现和维护

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

中介者模式可以非常方便地对模块或者对象进行解耦,但对象之间并非一定需要解耦。在实际项目中,模块或对象之间有一些依赖关系是很正常的。关键在于如何去衡量对象之间的耦合程度。一般来说,如果对象之间的复杂耦合确实导致调用和维护出现了困难,而且这些耦合度随项目的变化呈指数增长曲线,就可以考虑用中介者模式来重构代码

2.装饰者模式

装饰者模式可以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象

在传统的面向对象语言中,给对象添加功能常常使用继承的方式,但是继承的方式并不灵活,还会带来很多问题:一方面会导致超类和子类之间存在强耦合性,当超类改变时,子类也会随之改变;另一方面,继承这种功能复用方式通常被称为“白箱复用”,“白箱”是相对可见性而言的,在继承方式中,超类的内部细节是对子类可见的,继承常常被认为破坏了封装性。

使用继承还会带来另外一个问题,在完成一些功能复用的同时,有可能创建出大量的子类,使子类的数量呈爆炸性增长。

给对象动态地增加职责的方式称为装饰者模式。装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。跟继承相比,装饰者是一种更轻便灵活的做法,这是一种“即用即付”的方式。

2.1 JavaScript的装饰者

JavaScript语言动态改变对象相当容易,可以直接改写对象或者对象的某个方法,并不需要使用“类”来实现装饰者模式

var plane = {
    fire: function(){
        console.log('发射普通子弹')
    }
}

var missileDecorator = function(){
    console.log('发射导弹')
}

var atomDecorator = function(){
    console.log('发射原子弹')
}

var fire1 = plane.fire;

plane.fire = function(){
    fire1();
    missileDecorator();
}

var fire2 = plane.fire;

plane.fire = function(){
    fire2();
    atomDecorator();
}

plane.fire();
//分别输出: 发射普通子弹、发射导弹、发射原子弹
复制代码

2.2 装饰函数

在JavaScript中,几乎一切都是对象,其中函数又被称为一等对象。在JavaScript中可以很方便地给某个对象扩展属性和方法,但却很难在不改动某个函数源代码的情况下,给该函数添加一些额外的功能。在代码的运行期间,很难切入某个函数的执行环境

要想为函数添加一些功能,最简单粗暴的方式就是直接改写该函数,但这是最差的办法,直接违反了开放-封闭原则:

var a = function(){
    alert(1)
}

// 改成
var a = function(){
    alert(1)
    alert(2)
}
复制代码

很多时候并不想不碰原函数,也许原函数是由其他同事编写的,里面的实现非常杂乱。甚至在一个古老的项目中,这个函数的源代码被隐藏在一个我们不愿碰触的阴暗角落里。现在需要一个办法,在不改变函数源代码的情况下,能给函数增加功能,这正是开放-封闭原则给我们指出的光明道路

通过保存原引用的方式改写某个函数

var a = function(){
    alert(1)
}

var _a = a;

a = function(){
  _a();
  alert(2);
}

a();
复制代码

这是实际开发中很常见的一种做法,比如我们想给window绑定onload事件,但是又不确定这个事件是不是已经被其他人绑定过了,为了避免覆盖掉之前的window.onload函数中的行为,一般都会先保存好原先的window.onload,把它放入新的window.onload里执行

window.onload = function(){
    alert(1)
}

var _onload = window.onload || function(){};

window.onload = function(){
    _onload();
    alert(2)
}
复制代码

这样的代码当然是符合开放-封闭原则的,在增加新功能的时候,确实没有修改原来的wiondow.onload代码,但是这种方式存在以下两个问题

  • 必须维护_onload这个中间变量,虽然看起来并不起眼,但如果函数的装饰链较长,或者需要装饰的函数变多,这些中间变量的数量也会越来越多

  • 其实还遇到了this被劫持的问题,在window.onload的例子中没有这个烦恼,是因为调用普通函数_onload时,this也指向window,跟调用window.onload时一样(函数作为对象的方法被调用时,this指向该对象,所以此处this也指向window)。现在把window.onload换成document.getElementById,代码如下

var _getElementById = document.getElementById;

document.getElementById = function(id) {
    alert(1)
    return _getElementById(id)
}

var button = document.getElementById('button');
复制代码

执行这段代码,在弹出alert(1)后,紧接着控制台抛出了异常

异常发生在_getElementById(id)这句代码上,此时_getElementById是一个全局函数,当调用一个全局函数时,this指向window的,而document.getElementById方法的内部实现需要使用this引用,this在这个方法内预期是指向document,而不是window,这是错误发生的原因,所以使用现在的方式给函数增加功能并不保险

改进后的代码可以满足需求,手动把document当作上文下this传入_getElementById,

var _getElementById = document.getElementById;

document.getElementById = function() {
    alert(1)
    return _getElementById.apply(document, arguments)
}

var button = document.getElementById('button');
复制代码

但这样做显然很不方便,下面我们引入 AOP , 来提供一种完美的方法给函数动态增加功能

2.3 用 AOP 装饰函数

首先给出 Function.prototype.before 方法和 Function.prototype.after方法

Function.prototype.before = function(beforefn) {
    var _self = this;
    return function(){
        beforefn.apply(this, arguments);
        
        return _self.apply(this,arguments)
    }
}

Function.prototype.after = function(afterfn) {
    var _self = this;
    return function(){
        var ret = _self.apply(this,arguments)
        afterfn.apply(this, arguments);
        
        return ret
    }
}
复制代码
document.getElementById = document.getElementById.before(function(){
    alert(1)
});

var button = document.getElementById('button')


window.onload = function(){
    alert(1)
}

window.onload = (window.onload || function(){}).after(function(){
    alert(2)
}).after(function(){
    alert(3)
}).after(function(){
    alert(4)
})

复制代码

值得注意的是, 上面的AOP实现是在Function.prototype上添加before和after方法,但许多人不喜欢这种污染原型的方式,那么我们可以做一些变通,把原函数和新函数都作为参数传入before或者after方法

var before = function(fn, beforefn) {
    return function(){
        beforefn.apply(this, arguments);
        return fn.apply(this, arguments)
    }
}

var a = brfore(
    function(){alert(3)}
    function(){alert(2)}
)

a = before(a, function(){alert(1)});
a();
复制代码

2.4 AOP的应用实例

用AOP装饰函数的技巧在实际开发中非常有用。不论是业务代码的编写,还是在框架层面,都可以把行为依照职责分成粒度更细的函数,随后通过装饰把它们合并到一起,这有助于编写一个松耦合和高复用性的系统

2.4.1 数据统计上报

分离业务代码和数据统计代码,是AOP的经典应用之一。

var showLogin = function(){
    console.log('打开登录浮层')
    log(this.getAttribute('tag'))
}

var log = function(tag) {
    console.log('上报标签为:' + tag);
}

document.getElement('button').onclick = showLogin;
复制代码

在showLogin函数里,既要负责打开登录浮层,又要负责数据上报,这是两个层面的功能,在此处却被耦合在一个函数里。使用AOP分离之后,代码如下

Function.prototype.after = function(afterfn) {
    var _self = this;
    return function(){
        var ret = _self.apply(this,arguments)
        afterfn.apply(this, arguments);
        return ret
    }
}

var showLogin = function(){
    console.log('打开登录浮层')
}

var log = function(tag) {
    console.log('上报标签为:' + this.getAttribute('tag'));
}

showLogin = showLogin.after(log)

document.getElement('button').onclick = showLogin; 
复制代码

2.4.2 用AOP动态改变函数的参数

观察 Function.prototype.before 方法:

Function.prototype.before = function(beforefn) {
    var _self = this;
    return function(){
        beforefn.apply(this, arguments);
        return _self.apply(this,arguments)
    }
}
复制代码

beforefn和原函数_self公用一组参数列表arguments,当我们在beforefn的函数体内改变arguments的时候,原函数_self接收的参数列表自然也会变化

下面的例子展示了如何通过Function.prototype.before方法给函数func的参数params动态地添加属性b:

var func = function(params){
    console.log(params)
}

func = func.before(function(params){
    params.b = 'b'
})

func({a:'a'})
复制代码

有一个用于发起ajax的请求

var ajax = function(type, url, param){
    console.log(params)
}

var getToken = function(){
    return 'Token'
}

ajax = ajax.before(function(type, url, param){
    param.token = getToken()
})

ajax('get', 'http://xxx.com', {name: 'xiaoming'})

复制代码

可以看到,用AOP方式给ajax函数动态装饰上Token参数,保证了ajax函数是一个相对纯净的函数,提高了ajax函数的可复用性,它在被迁往其他项目的时候,不需要做任何修改

2.4.3 插件式的表单验证

用户名: <input id="username" type="text"/>
密码: <input id="password" type="password"/>
<input id="submitBtn" type="button" value="提交"/>

var username = document.getElementById('username'),
    password = document.getElementById('password'),
    submitBtn = document.getElementById('submitBtn')
    
var formSubmit = function(){
    if (username.value === '') {
        return alter('用户名不能为空')
    }
    if (password.value === '') {
        return alter('密码不能为空')
    }
    
    var params = {
        username: username.value,
        password: password.value
    }
    
    ajax('http://xxx.com/login', params)
}

submitBtn.onclick = function(){
    formSubmit();
}
复制代码

formSubmit函数在此处承担了两个职责,除了提交ajax请求之外,还要验证用户输入的合法性。这种代码一来会造成函数臃肿,职责混乱,二来谈不上任何可复用性。

分离校验输入和提交的ajax请求的代码

var validata = function(){
    if (username.value === '') {
        alter('用户名不能为空')
        return false
    }
    if (password.value === '') {
        alter('密码不能为空')
        return fasle
    }
}

var formSubmit = function(){
    if (validata() === false) {
      return      
    }
    var params = {
        username: username.value,
        password: password.value
    }
    
    ajax('http://xxx.com/login', params)
}

submitBtn.onclick = function(){
    formSubmit();
}
复制代码

代码已经有了一些改进,把校验逻辑都放到了validata函数中,但formSubmit函数的内部还要计算validata函数的返回值

接下来进一步优化代码,使validata和formSubmit完全分离开来。首先要改写Function.prototype.before,如果beforefn的执行结果返回false,表示不再执行后面的原函数

Function.prototype.before = function(beforefn) {
    var _self = this;
    return function(){
        if (beforefn.apply(this, arguments) === false) {
            return;    
        }
        return _self.apply(this,arguments)
    }
}

var validata = function(){
    if (username.value === '') {
        alter('用户名不能为空')
        return false
    }
    if (password.value === '') {
        alter('密码不能为空')
        return fasle
    }
}

var formSubmit = function(){
    var params = {
        username: username.value,
        password: password.value
    }
    
    ajax('http://xxx.com/login', params)
}

formSubmit = formSubmit.before(validata)

submitBtn.onclick = function(){
    formSubmit();
}
复制代码

校验输入和提交表单的代码完全分离开来,不再有任何耦合关系,formSubmit = formSubmit.before(validata) 这句代码,如同把校验规则动态接在 formSubmit 函数之前,validata 成为一个即插即用的函数,它甚至可以被写成配置文件的形式,这有利于分开维护这两个函数。再利用策略模式稍加改造,就可以把这些校验规则都写成插件的形式,用在不同的项目当中

值得注意的是,因为函数通过Function.prototype.before或者Function.prototype.after被装饰之后,返回的实际上是一个新的函数,如果在原函数上保存了一些属性,那么这些属性会丢失。另外,这种装饰方式也叠加了函数的作用域,如果装饰的链条过长,性能上也会受到一些影响

2.5 装饰者模式和代理模式

装饰者模式和代理模式的结构看起来非常相像,这两种模式都描述了怎样为对象提供一定程度上的间接引用,它们的实现部分都保留了对另外一个对象的引用,并且向那个对象发送请求

代理模式和装饰者模式最重要的区别在于它们的意图和设计目的。代理模式的目的是,当直接访问本体不方便或者不符合需要时,为这个本体提供一个替代者。本体定义了关键功能,而代理提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情。装饰者模式的作用就是为对象动态加入行为。换句话说,代理模式强调一种关系(Proxy和它的实体之间的关系),这种关系可以静态的表达,也就是说,这种关系在一开始就可以被确定。而装饰者模式用于一开始不能确定对象的全部功能时。代理模式通常只有一层代理-本体的引用,而装饰者模式经常会形成一条长长的装饰链

通过数据上报、动态改变函数参数以及插件式的表单验证这几个例子,了解了装饰函数,它是JavaScript中独特的装饰者模式。这种模式在实际开发中非常有用。

猜你喜欢

转载自juejin.im/post/7019678675121471495
今日推荐