面向对象的设计原则

前面的话

  面向对象的设计原则,可以说每种设计模式都是为了让代码迎合其中一个或多个原则而出现的, 它们本身已经融入了设计模式之中,给面向对象编程指明了方向。适合javascript开发的设计原则包括是单一职责原则、最少知识原则和开放封闭原则。本文将详细介绍面向对象的设计原则

 

单一职责原则

  就一个类而言,应该仅有一个引起它变化的原因。在javascript中,需要用到类的场景并不太多,单一职责原则更多地是被运用在对象或者方法级别上

  单一职责原则(SRP)的职责被定义为“引起变化的原因”。如果有两个动机去改写一个方法,那么这个方法就具有两个职责。每个职责都是变化的一个轴线,如果一个方法承担了过多的职责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大。此时,这个方法通常是一个不稳定的方法,修改代码总是一件危险的事情,特别是当两个职责耦合在一起的时候,一个职责发生变化可能会影响到其他职责的实现,造成意想不到的破坏,这种耦合性得到的是低内聚和脆弱的设计。因此,SRP原则体现为:一个对象(方法)只做一件事情

  SRP原则在很多设计模式中都有着广泛的运用,例如代理模式、迭代器模式、单例模式和装饰者模式

【代理模式】

  通过增加虚拟代理的方式,把预加载图片的职责放到代理对象中,而本体仅仅负责往页面中添加img标签,这也是它最原始的职责

  myImage负责往页面中添加img标签:

var myImage = (function(){
    var imgNode = document.createElement( 'img' );
    document.body.appendChild( imgNode );
    return {
        setSrc: function( src ){

            imgNode.src = src;
        }
    }
})();

  proxyImage负责预加载图片,并在预加载完成之后把请求交给本体 myImage:

var proxyImage = (function(){
    var img = new Image;
    img.onload = function(){
        myImage.setSrc( this.src );
    }
    return {
        setSrc: function( src ){
            myImage.setSrc( 'file://loading.gif' );
            img.src = src;
        }
    }
})();
proxyImage.setSrc( 'http://test.jpg' );

  把添加img标签的功能和预加载图片的职责分开放到两个对象中,这两个对象各自都只有一个被修改的动机。在它们各自发生改变的时候,也不会影响另外的对象

【迭代器模式】

  有这样一段代码,先遍历一个集合,然后往页面中添加一些div,这些div的innerHTML分别对应集合里的元素:

var appendDiv = function( data ){
  for ( var i = 0, l = data.length; i < l; i++ ){ 
    var div = document.createElement( 'div' ); 
    div.innerHTML = data[ i ]; 
    document.body.appendChild( div );
  }
};
appendDiv( [ 1, 2, 3, 4, 5, 6 ] );

  这其实是一段很常见的代码,经常用于ajax请求之后,在回调函数中遍历ajax请求返回的数据,然后在页面中渲染节点。appendDiv函数本来只是负责渲染数据,但是在这里它还承担了遍历聚合对象data的职责。如果有一天cgi返回的data数据格式从array变成了object,那遍历data的代码就会出现问题,必须改成for in的方式,这时候必须去修改appendDiv里的代码,否则因为遍历方式的改变,导致不能顺利往页面中添加div节点

  有必要把遍历data的职责提取出来,这正是迭代器模式的意义,迭代器模式提供了一种方法来访问聚合对象,而不用暴露这个对象的内部表示。

  当把迭代聚合对象的职责单独封装在each函数中后,即使以后还要增加新的迭代方式,只需要修改each函数即可,appendDiv函数不会受到牵连,代码如下:

var each = function( obj, callback ) {
    var value,
    i = 0,
    length = obj.length,
    isArray = isArraylike( obj ); // isArraylike 函数未实现
    if ( isArray ) { // 迭代类数组
        for ( ; i < length; i++ ) {
            callback.call( obj[ i ], i, obj[ i ] );
        }
    } else {
        for ( i in obj ) { // 迭代object 对象
            value = callback.call( obj[ i ], i, obj[ i ] );
        }
    }
    return obj;
};

var appendDiv = function( data ){
    each( data, function( i, n ){
        var div = document.createElement( 'div' );
        div.innerHTML = n;
        document.body.appendChild( div );
    });
};

appendDiv( [ 1, 2, 3, 4, 5, 6 ] );
appendDiv({a:1,b:2,c:3,d:4} );

【单例模式】

  下面是一段代码

var createLoginLayer = (function(){
    var div;
    return function(){
        if ( !div ){
            div = document.createElement( 'div' );
            div.innerHTML = '我是登录浮窗';
            div.style.display = 'none';
            document.body.appendChild( div );
        }
        return div;
    }
})();

  现在把管理单例的职责和创建登录浮窗的职责分别封装在两个方法里,这两个方法可以独立变化而互不影响,当它们连接在一起的时候,就完成了创建唯一登录浮窗的功能,下面的代码显然是更好的做法:

var getSingle = function( fn ){ // 获取单例
    var result;
    return function(){
        return result || ( result = fn .apply(this, arguments ) );
    }
};
var createLoginLayer = function(){ // 创建登录浮窗
    var div = document.createElement( 'div' );
    div.innerHTML = '我是登录浮窗';
    document.body.appendChild( div );
    return div;
};

var createSingleLoginLayer = getSingle( createLoginLayer );
var loginLayer1 = createSingleLoginLayer();
var loginLayer2 = createSingleLoginLayer();
alert ( loginLayer1 === loginLayer2 ); // 输出: true

【装饰者模式】

  使用装饰者模式时,通常让类或者对象一开始只具有一些基础的职责,更多的职责在代码运行时被动态装饰到对象上面。装饰者模式可以为对象动态增加职责,从另一个角度来看, 这也是分离职责的一种方式

  下面把数据上报的功能单独放在一个函数里,然后把这个函数动态装饰到业务函数上面:

<button tag="login" id="button">点击打开登录浮层</button>
<script>
    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(){
        console.log( '上报标签为: ' + this.getAttribute( 'tag' ) );

    };
    document.getElementById( 'button' ).onclick = showLogin.after( log );
// 打开登录浮层之后上报数据

  SRP原则是所有原则中最简单也是最难正确运用的原则之一。要明确的是,并不是所有的职责都应该一一分离。一方面,如果随着需求的变化,有两个职责总是同时变化,那就不必分离他们。比如在ajax请求的时候,创建xhr对象和发送xhr请求几乎总是在一起的,那么创建xhr对象的职责和发送xhr请求的职责就没有必要分开。另一方面,职责的变化轴线仅当它们确定会发生变化时才具有意义,即使两个职责已经被耦合在一起,但它们还没有发生改变的征兆,那么也许没有必要主动分离它们,在代码需要重构的时候再进行分离也不迟

  在人的常规思维中,总是习惯性地把一组相关的行为放到一起,如何正确地分离职责不是一件容易的事情。在实际开发中,因为种种原因违反SRP的情况并不少见。比如jQuery的attr等方法,就是明显违反SRP原则的做法。jQuery的attr是个非常庞大的方法,既负责赋值,又负责取值,这对于jQuery的维护者来说,会带来一些困难,但对于jQuery的用户来说,却简化了用户的使用。在方便性与稳定性之间要有一些取舍。具体是选择方便性还是稳定性,并没有标准答案,而是要取决于具体的应用环境

  SRP原则的优点是降低了单个类或者对象的复杂度,按照职责把对象分解成更小的粒度,这有助于代码的复用,也有利于进行单元测试。当一个职责需要变更的时候,不会影响到其他的职责。但SRP原则也有一些缺点,最明显的是会增加编写代码的复杂度。当按照职责把对象分解成更小的粒度之后,实际上也增大了这些对象之间相互联系的难度

 

最少知识原则

  最少知识原则(LKP)说的是一个软件实体应当尽可能少地与其他实体发生相互作用。这里的软件实体是一个广义的概念,不仅包括对象,还包括系统、类、模块、函数、变量等

  某军队中的将军需要挖掘一些散兵坑。下面是完成任务的一种方式:将军可以通知上校让他叫来少校,然后让少校找来上尉,并让上尉通知一个军士,最后军士唤来一个士兵,然后命令士兵挖掘一些散兵坑。这种方式十分荒谬,不是吗?不过,还是先来看一下这个过程的等价代码:

gerneral.getColonel(c).getMajor(m).getCaptain(c).getSergeant(s).getPrivate(p).digFoxhole();

  让代码通过这么长的消息链才能完成一个任务,这就像让将军通过那么多繁琐的步骤才能命令别人挖掘散兵坑一样荒谬!而且,这条链中任何一个对象的改动都会影响整条链的结果。最有可能的是,将军自己根本就不会考虑挖散兵坑这样的细节信息。但是如果将军真的考虑了这个问题的话,他一定会通知某个军官:“我不关心这个工作如何完成,但是你得命令人去挖散兵坑。”

  单一职责原则指导我们把对象划分成较小的粒度,这可以提高对象的可复用性。但越来越多的对象之间可能会产生错综复杂的联系,如果修改了其中一个对象,很可能会影响到跟它相互引用的其他对象。对象和对象耦合在一起,有可能会降低它们的可复用性。

  最少知识原则要求我们在设计程序时,应当尽量减少对象之间的交互。如果两个对象之间不必彼此直接通信,那么这两个对象就不要发生直接的相互联系。常见的做法是引入一个第三者对象,来承担这些对象之间的通信作用。如果一些对象需要向另一些对象发起请求,可以通过第三者对象来转发这些请求

  最少知识原则在设计模式中体现得最多的地方是中介者模式和外观模式

【中介者模式】

  在世界杯期间购买足球彩票,如果没有博彩公司作为中介,上千万的人一起计算赔率和输赢绝对是不可能的事情。博彩公司作为中介,每个人都只和博彩公司发生关联,博彩公司会根据所有人的投注情况计算好赔率,彩民们赢了钱就从博彩公司拿,输了钱就赔给博彩公司。中介者模式很好地体现了最少知识原则。通过增加一个中介者对象,让所有的相关对象都通过中介者对象来通信,而不是互相引用。所以,当一个对象发生改变时,只需要通知中介者对象即可

【外观模式】

  外观模式主要是为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使子系统更加容易使用

  外观模式的作用是对客户屏蔽一组子系统的复杂性。外观模式对客户提供一个简单易用的高层接口,高层接口会把客户的请求转发给子系统来完成具体的功能实现。大多数客户都可以通过请求外观接口来达到访问子系统的目的。但在一段使用了外观模式的程序中,请求外观并不是强制的。如果外观不能满足客户的个性化需求,那么客户也可以选择越过外观来直接访问子系统

  拿全自动洗衣机的一键洗衣按钮举例,这个一键洗衣按钮就是一个外观。如果是老式洗衣机,客户要手动选择浸泡、洗衣、漂洗、脱水这4个步骤。如果这种洗衣机被淘汰了,新式洗衣机的漂洗方式发生了改变,那还得学习新的漂洗方式。而全自动洗衣机的好处很明显,不管洗衣机内部如何进化,客户要操作的,始终只是一个一键洗衣的按钮。这个按钮就是为一组子系统所创建的外观。但如果一键洗衣程序设定的默认漂洗时间是20分钟,而客户希望这个漂洗时间是30分钟,那么客户自然可以选择越过一键洗衣程序,自己手动来控制这些“子系统”运转。外观模式容易跟普通的封装实现混淆。这两者都封装了一些事物,但外观模式的关键是定义一个高层接口去封装一组“子系统”。子系统在C++或者Java中指的是一组类的集合,这些类相互协作可以组成系统中一个相对独立的部分。在javascript中通常不会过多地考虑“类”,如果将外观模式映射到javascript中,这个子系统至少应该指的是一组函数的集合

  最简单的外观模式应该是类似下面的代码:

var A = function(){
  a1();
  a2();
}
var B = function(){
  b1();
  b2();
}
var facade =function(){
  A();
  B();
}
facade();

  许多javascript设计模式的图书或者文章喜欢把jQuery的$.ajax函数当作外观模式的实现,这是不合适的。如果$.ajax函数属于外观模式,那几乎所有的函数都可以被称为“外观模式”。问题是根本没有办法越过$.ajax“外观”去直接使用该函数中的某一段语句

  现在再来看看外观模式和最少知识原则之间的关系。外观模式的作用主要有两点

  1、为一组子系统提供一个简单便利的访问入口

  2、隔离客户与复杂子系统之间的联系,客户不用去了解子系统的细节。从第二点来,外观模式是符合最少知识原则的

  封装在很大程度上表达的是数据的隐藏。一个模块或者对象可以将内部的数据或者实现细节隐藏起来,只暴露必要的接口API供外界访问。对象之间难免产生联系,当一个对象必须引用另外一个对象的时候,可以让对象只暴露必要的接口,让对象之间的联系限制在最小的范围之内。同时,封装也用来限制变量的作用域。在javascript中对变量作用域的规定是:

  1、变量在全局声明,或者在代码的任何位置隐式申明(不用var),则该变量在全局可见;

  2、变量在函数内显式申明(使用var),则在函数内可见。把变量的可见性限制在一个尽可能小的范围内,这个变量对其他不相关模块的影响就越小,变量被改写和发生冲突的机会也越小。这也是广义的最少知识原则的一种体现

  假设要编写一个具有缓存效果的计算乘积的函数function mult(){},需要一个对象var cache = {}来保存已经计算过的结果。cache对象显然只对mult有用,把cache对象放在mult形成的闭包中,显然比把它放在全局作用域更加合适,代码如下:

var mult = (function(){
    var cache = {};
    return function(){
        var args = Array.prototype.join.call( arguments, ',' );
        if ( cache[ args ] ){
            return cache[ args ];
        }
        var a = 1;
        for ( var i = 0, l = arguments.length; i < l; i++ ){

            a = a * arguments[i];
        }
        return cache[ args ] = a;
    }
})();
mult( 1, 2, 3 ); // 输出: 6

  虽然遵守最小知识原则减少了对象之间的依赖,但也有可能增加一些庞大到难以维护的第三者对象。跟单一职责原则一样,在实际开发中,是否选择让代码符合最少知识原则,要根据具体的环境来定

 

开放封闭原则

  在面向对象的程序设计中,开放——封闭原则(OCP)是最重要的一条原则。很多时候,一个程序具有良好的设计,往往说明它是符合开放——封闭原则的。开放——封闭原则的定义如下:软件实体(类、模块、函数)等应该是可以扩展的,但是不可修改

  假设我们是一个大型Web项目的维护人员,在接手这个项目时,发现它已经拥有10万行以上的javascript代码和数百个JS文件。不久后接到了一个新的需求,即在window.onload函数中打印出页面中的所有节点数量。打开文本编辑器,搜索出window.onload函数在文件中的位置,在函数内部添加以下代码:

window.onload=function(){
  //原有代码略
  console.log(document.getElementsByTagName('*').length);
};

  在项目需求变迁的过程中,经常会找到相关代码,然后改写它们。这似乎是理所当然的事情,不改动代码怎么满足新的需求呢?想要扩展一个模块,最常用的方式当然是修改它的源代码。如果一个模块不允许修改,那么它的行为常常是固定的。然而,改动代码是一种危险的行为,也许都遇到过bug越改越多的场景。刚刚改好了一个bug,但是又在不知不觉中引发了其他的bug

  如果目前的window.onload函数是一个拥有500行代码的巨型函数,里面密布着各种变量和交叉的业务逻辑,而需求又不仅仅是打印一个log这么简单。那么“改好一个bug,引发其他bug”这样的事情就很可能会发生。永远不知道刚刚的改动会有什么副作用,很可能会引发一系列的连锁反应

  那么,有没有办法在不修改代码的情况下,就能满足新需求呢?通过增加代码,而不是修改代码的方式,来给window.onload函数添加新的功能,代码如下:

Function.prototype.after = function( afterfn ){
    var __self = this;
    return function(){
        var ret = __self.apply( this, arguments );
        afterfn.apply( this, arguments );
        return ret;
    }
};
window.onload = ( window.onload || function(){} ).after(function(){
    console.log( document.getElementsByTagName( '*' ).length );
});

  通过动态装饰函数的方式,完全不用理会从前window.onload函数的内部实现,无论它的实现优雅或是丑陋。就算作为维护者,拿到的是一份混淆压缩过的代码也没有关系。只要它从前是个稳定运行的函数,那么以后也不会因为我们的新增需求而产生错误。新增的代码和原有的代码可以井水不犯河水

  现在引出开放——封闭原则的思想:当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,但是不允许改动程序的源代码

  过多的条件分支语句是造成程序违反开放——封闭原则的一个常见原因。每当需要增加一个新的if语句时,都要被迫改动原函数。把if换成switch-case是没有用的,这是一种换汤不换药的做法。实际上,每当看到一大片的if或者swtich-case语句时,第一时间就应该考虑,能否利用对象的多态性来重构它们

  利用对象的多态性来让程序遵守开放——封闭原则,是一个常用的技巧。下面先提供一段不符合开放——封闭原则的代码。每当增加一种新的动物时,都需要改动makeSound函数的内部实现:

var makeSound = function( animal ){
    if ( animal instanceof Duck ){
        console.log( '嘎嘎嘎' );
    }else if ( animal instanceof Chicken ){
        console.log( '咯咯咯' );
    }
};

var Duck = function(){};
var Chicken = function(){};
makeSound( new Duck() ); // 输出:嘎嘎嘎
makeSound( new Chicken() ); // 输出:咯咯咯

  动物世界里增加一只狗之后,makeSound函数必须改成:

var makeSound = function( animal ){
    if ( animal instanceof Duck ){
        console.log( '嘎嘎嘎' );
    }else if ( animal instanceof Chicken ){
        console.log( '咯咯咯' );
    }else if ( animal instanceof Dog ){ // 增加跟狗叫声相关的代码
        console.log('汪汪汪' );
    }
};
var Dog = function(){};
makeSound( new Dog() ); // 增加一只狗

  利用多态的思想,把程序中不变的部分隔离出来(动物都会叫),然后把可变的部分封装起来(不同类型的动物发出不同的叫声),这样一来程序就具有了可扩展性。想让一只狗发出叫声时,只需增加一段代码即可,而不用去改动原有的makeSound函数:

var makeSound = function( animal ){
    animal.sound();
};
var Duck = function(){};
Duck.prototype.sound = function(){
    console.log( '嘎嘎嘎' );
};
var Chicken = function(){};
Chicken.prototype.sound = function(){
    console.log( '咯咯咯' );
};
makeSound( new Duck() ); // 嘎嘎嘎
makeSound( new Chicken() ); // 咯咯咯
/********* 增加动物狗,不用改动原有的makeSound 函数 ****************/
var Dog = function(){};
Dog.prototype.sound = function(){
    console.log( '汪汪汪' );
};
makeSound( new Dog() ); // 汪汪汪

  遵守开放——封闭原则的规律,最明显的就是找出程序中将要发生变化的地方,然后把变化封装起来。通过封装变化的方式,可以把系统中稳定不变的部分和容易变化的部分隔离开来。在系统的演变过程中,只需要替换那些容易变化的部分,如果这些部分是已经被封装好的,那么替换起来也相对容易。而变化部分之外的就是稳定的部分。在系统的演变过程中,稳定的部分是不需要改变的

  由于每种动物的叫声都不同,所以动物具体怎么叫是可变的,于是把动物具体怎么叫的逻辑从makeSound函数中分离出来。而动物都会叫这是不变的,makeSound函数里的实现逻辑只跟动物都会叫有关,这样一来,makeSound就成了一个稳定和封闭的函数。除了利用对象的多态性之外,还有其他方式可以帮助编写遵守开放——封闭原则的代码

【放置挂钩】

  放置挂钩(hook)也是分离变化的一种方式。在程序有可能发生变化的地方放置一个挂钩,挂钩的返回结果决定了程序的下一步走向。这样一来,原本的代码执行路径上就出现了一个分叉路口,程序未来的执行方向被预埋下多种可能性。

  由于子类的数量是无限制的,总会有一些“个性化”的子类迫使不得不去改变已经封装好的算法骨架。于是可以在父类中的某个容易变化的地方放置挂钩,挂钩的返回结果由具体子类决定。这样一来,程序就拥有了变化的可能

【使用回调函数】

  在javascript中,函数可以作为参数传递给另外一个函数,这是高阶函数的意义之一。在这种情况下,通常会把这个函数称为回调函数。在javascript版本的设计模式中,策略模式和命令模式等都可以用回调函数轻松实现

  回调函数是一种特殊的挂钩。可以把一部分易于变化的逻辑封装在回调函数里,然后把回调函数当作参数传入一个稳定和封闭的函数中。当回调函数被执行的时候,程序就可以因为回调函数的内部逻辑不同,而产生不同的结果

  比如,通过ajax异步请求用户信息之后要做一些事情,请求用户信息的过程是不变的,而获取到用户信息之后要做什么事情,则是可能变化的:

var getUserInfo = function( callback ){
    $.ajax( 'http:// xxx.com/getUserInfo', callback );
};
getUserInfo( function( data ){
    console.log( data.userName );
});
getUserInfo( function( data ){
    console.log( data.userId );
});

  另外一个例子是关于Array.prototype.map的。在不支持Array.prototype.map的浏览器中,可以简单地模拟实现一个map函数

  arrayMap函数的作用是把一个数组“映射”为另外一个数组。映射的步骤是不变的,而映射的规则是可变的,于是把这部分规则放在回调函数中,传入arrayMap函数:

var arrayMap = function( ary, callback ){
    var i = 0,
    length = ary.length,
    value,
    ret = [];
    for ( ; i < length; i++ ){
        value = callback( i, ary[ i ] );
        ret.push( value );
    }
    return ret;
}
var a = arrayMap( [ 1, 2, 3 ], function( i, n ){
    return n * 2;
});
var b = arrayMap( [ 1, 2, 3 ], function( i, n ){
    return n * 3;
});

console.log( a ); // 输出:[ 2, 4, 6 ]
console.log( b ); // 输出:[ 3, 6, 9 ]

  有一种说法是,设计模式就是给做的好的设计取个名字。几乎所有的设计模式都是遵守开放——封闭原则的。不管是具体的各种设计模式,还是更抽象的面向对象设计原则,比如单一职责原则、最少知识原则、依赖倒置原则等,都是为了让程序遵守开放——封闭原则而出现的。可以这样说,开放——封闭原则是编写一个好程序的目标,其他设计原则都是达到这个目标的过程

【发布——订阅模式】

  发布——订阅模式用来降低多个对象之间的依赖关系,它可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。当有新的订阅者出现时,发布者的代码不需要进行任何修改;同样当发布者需要改变时,也不会影响到之前的订阅者

【模板方法模式】

  模板方法模式是一种典型的通过封装变化来提高系统扩展性的设计模式。在一个运用了模板方法模式的程序中,子类的方法种类和执行顺序都是不变的,所以把这部分逻辑抽出来放到父类的模板方法里面;而子类的方法具体怎么实现则是可变的,于是把这部分变化的逻辑封装到子类中。通过增加新的子类,便能给系统增加新的功能,并不需要改动抽象父类以及其他的子类,这也是符合开放——封闭原则的

【策略模式】

  策略模式和模板方法模式是一对竞争者。在大多数情况下,它们可以相互替换使用。模板方法模式基于继承的思想,而策略模式则偏重于组合和委托。策略模式将各种算法都封装成单独的策略类,这些策略类可以被交换使用。策略和使用策略的客户代码可以分别独立进行修改而互不影响。增加一个新的策略类也非常方便,完全不用修改之前的代码

【代理模式】

  拿预加载图片举例,现在已有一个给图片设置src的函数myImage,想为它增加图片预加载功能时,一种做法是改动myImage函数内部的代码,更好的做法是提供一个代理函数proxyMyImage,代理函数负责图片预加载,在图片预加载完成之后,再将请求转交给原来的myImage函数,myImage在这个过程中不需要任何改动。预加载图片的功能和给图片设置src的功能被隔离在两个函数里,它们可以单独改变而互不影响。myImage不知晓代理的存在,它可以继续专注于自己的职责——给图片设置src

【职责链模式】

  把一个巨大的订单函数分别拆成了500元订单、200元订单以及普通订单的3个函数。这3个函数通过职责链连接在一起,客户的请求会在这条链条里面依次传递:

var order500yuan = new Chain(function( orderType, pay, stock ){
// 具体代码略
});

var order200yuan = new Chain(function( orderType, pay, stock ){
// 具体代码略
});

var orderNormal = new Chain(function( orderType, pay, stock ){
// 具体代码略
});

order500yuan.setNextSuccessor( order200yuan ).setNextSuccessor( orderNormal ); 
order500yuan.passRequest( 1, true, 10 );    // 500 元定金预购,得到 100 优惠券

  可以看到,当增加一个新类型的订单函数时,不需要改动原有的订单函数代码,只需要在链条中增加一个新的节点

  在职责链模式代码中,开放——封闭原则要求只能通过增加源代码的方式扩展程序的功能,而不允许修改源代码。那往职责链中增加一个新的100元订单函数节点时,不也必须改动设置链条的代码吗?代码如下:

order500yuan.setNextSuccessor(order200yuan).setNextSuccessor(orderNormal);

  变为:

order500yuan.setNextSuccessor(order200yuan).setNextSuccessor(order100yuan).setNextSuccessor(orderNormal);

  实际上,让程序保持完全封闭是不容易做到的。就算技术上做得到,也需要花费太多的时间和精力。而且让程序符合开放——封闭原则的代价是引入更多的抽象层次,更多的抽象有可能会增大代码的复杂度。更何况,有一些代码是无论如何也不能完全封闭的,总会存在一些无法对其封闭的变化

  作为程序员,可以做到的有下面两点

  1、挑选出最容易发生变化的地方,然后构造抽象来封闭这些变化

  2、在不可避免发生修改的时候,尽量修改那些相对容易修改的地方。拿一个开源库来说,修改它提供的配置文件,总比修改它的源代码来得简单

 

posted @ 2017-12-18 00:21  小火柴的蓝色理想  阅读(1435)  评论(3编辑  收藏  举报