第二十章 开放-封闭原则
在面向对象的程序设计中,开发-封闭原则(OCP)是最重要的一条原则。一个程序具有良好的设计,往往说明它是符合开放-封闭原则的。
开放-封闭原则的定义是:
- 软件实体(类、模块、函数)等应该是可以扩展的,但是不可以修改。
20.1 扩展 window.onload 函数
假设我们是一个大型项目的维护人员,在接手这个项目时,发现它已经拥有了 10 万行以上的 JavaScript 代码和数百个 js 文件。
不久我们接到一个新需求,需要在 window.onload 函数中打印页面中的所有节点数量。于是我们打开编辑器,搜索到 window.onload 函数在文件中的问题,在函数内部添加以下代码:
js1window.onload=function(){ 2 ... 3 //原有代码略 4 console.log(document.getElementsByTagName('*').length) 5}
在项目需求变迁中,我们经常会找到相关代码,然后改写它们。想要扩展一个模块,最常用的方式自然是修改它的源代码。
然而,修改代码是一种危险的行为,也许我们都遇到过 bug 越改越多的场景,刚刚改好一个 bug,又不知不觉引发新的 bug。
如果我们的 window.onload 是拥有 500 行的巨型函数,里面密布着各种变量和交叉的业务逻辑,而我们的需求也非常复杂,那我们改好一个 bug,出现一个新的 bug 的事情可能就会发生。我们永远不知道刚刚的改动有什么副作用。
那么,在不修改代码的情况下,我们需要怎样满足新的需求呢?我们需要通过增加代码而不是修改原来代码的方式来给 window.onload 函数添加新的功能。
js1Function.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 函数的内部实现,这是不符合开放-封闭原则的:
js1 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 这个函数,而可变的部分是不同类型的动物发出不同的叫声。
当我们希望再增加一只狗时,就只需要增加代码而不需要修改原来的代码了。
js1 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() ); // 汪汪汪