第二十章 开放-封闭原则

在面向对象的程序设计中,开发-封闭原则(OCP)是最重要的一条原则。一个程序具有良好的设计,往往说明它是符合开放-封闭原则的。

开放-封闭原则的定义是:

  • 软件实体(类、模块、函数)等应该是可以扩展的,但是不可以修改。

20.1 扩展 window.onload 函数

假设我们是一个大型项目的维护人员,在接手这个项目时,发现它已经拥有了 10 万行以上的 JavaScript 代码和数百个 js 文件。

不久我们接到一个新需求,需要在 window.onload 函数中打印页面中的所有节点数量。于是我们打开编辑器,搜索到 window.onload 函数在文件中的问题,在函数内部添加以下代码:

js
1window.onload=function(){
2  ...
3  //原有代码略
4  console.log(document.getElementsByTagName('*').length)
5}

在项目需求变迁中,我们经常会找到相关代码,然后改写它们。想要扩展一个模块,最常用的方式自然是修改它的源代码。

然而,修改代码是一种危险的行为,也许我们都遇到过 bug 越改越多的场景,刚刚改好一个 bug,又不知不觉引发新的 bug。

如果我们的 window.onload 是拥有 500 行的巨型函数,里面密布着各种变量和交叉的业务逻辑,而我们的需求也非常复杂,那我们改好一个 bug,出现一个新的 bug 的事情可能就会发生。我们永远不知道刚刚的改动有什么副作用。

那么,在不修改代码的情况下,我们需要怎样满足新的需求呢?我们需要通过增加代码而不是修改原来代码的方式来给 window.onload 函数添加新的功能。

js
1Function.prototype.after = function (afterfn) {
2  var __self = this
3  return function () {
4    var ret = __self.apply(this, arguments)
5    afterfn.apply(this, arguments)
6    return ret
7  }
8}
9
10window.onload = (window.onload || function () {}).after(function () {
11  console.log(document.getElementsByTagName('*').length)
12})

通过动态装饰函数的方式,我们完全不用理会从来的函数的内部实现,只要它从前是一个稳定的函数,那么就不会因为我们的新增需求而产生错误。

20.2 开放和封闭

修改原有的代码和用动态装饰函数增加一段新的代码,哪种效果更好,已经不言而喻。

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

现实中有个故事是这样的:

两家企业都生产肥皂,但是他们的生产线有几率会生产出空盒。

第一家企业的老板从国外花费巨额资金引进技术来改造生产线,终于解决了问题。

另一家企业的老板则买了一个大风扇在旁边吹,空盒肥皂就会被吹走。

这个故事告诉我们,相比修改源代码,如果能通过增加几行代码的形式解决问题,显然更加简单和优雅。而且增加代码并不会影响原有系统的稳定。

20.3 用对象的多态性消除条件分支

过多的条件分支语句是造成程序违反开放-封闭原则的一个常见原因。每当需要增加一个新的 if 语句时,都要被迫改动原函数。把 if 换成 switch-case 是没有用的,这是一种换汤不换药的做法。

当我们看到一大片的 if 或者 swtich-case 语句时,第一时间就应该考虑,能否利用对象的多态性来重构它们。

利用对象的多态性来让程序遵守开放-封闭原则,是一个常用的技巧。

比如下面的代码,每当我们增加一种动物时,都需要增加 makeSound 函数的内部实现,这是不符合开放-封闭原则的:

js
1        var makeSound = function( animal ){
2            if ( animal instanceof Duck ){
3              console.log( ’嘎嘎嘎’ );
4            }else if ( animal instanceof Chicken ){
5              console.log( ’咯咯咯’ );
6            }
7        };
8
9        var Duck = function(){};
10        var Chicken = function(){};
11
12        makeSound( new Duck() );      // 输出:嘎嘎嘎
13        makeSound( new Chicken() );   // 输出:咯咯咯

利用多态的思想,我们可以把程序中不变的部分隔离出来,然后把可变的部分封装起来。这样一来程序就有了可扩展性。上面代码的不变的部分是 makeSound 这个函数,而可变的部分是不同类型的动物发出不同的叫声。

当我们希望再增加一只狗时,就只需要增加代码而不需要修改原来的代码了。

js
1        var makeSound = function( animal ){
2            animal.sound();
3        };
4
5        var Duck = function(){};
6
7        Duck.prototype.sound = function(){
8            console.log( ’嘎嘎嘎’ );
9        };
10
11        var Chicken = function(){};
12
13        Chicken.prototype.sound = function(){
14            console.log( ’咯咯咯’ );
15        };
16
17        makeSound( new Duck() );     // 嘎嘎嘎
18        makeSound( new Chicken() );  // 咯咯咯
19
20        /********* 增加动物狗,不用改动原有的makeSound函数 ****************/
21
22        var Dog = function(){};
23        Dog.prototype.sound = function(){
24            console.log( ’汪汪汪’ );
25        };
26
27        makeSound( new Dog() );     // 汪汪汪