深入理解JavaScript系列(8) S.O.L.I.D五大原则之里氏替换原则LSP


Posted in Javascript onJanuary 15, 2012

前言
本章我们要讲解的是S.O.L.I.D五大原则JavaScript语言实现的第3篇,里氏替换原则LSP(The Liskov Substitution Principle )。

英文原文:http://freshbrewedcode.com/derekgreer/2011/12/31/solid-javascript-the-liskov-substitution-principle/
复制代码
开闭原则的描述是:

Subtypes must be substitutable for their base types.
派生类型必须可以替换它的基类型。
复制代码
在面向对象编程里,继承提供了一个机制让子类和共享基类的代码,这是通过在基类型里封装通用的数据和行为来实现的,然后已经及类型来声明更详细的子类型,为了应用里氏替换原则,继承子类型需要在语义上等价于基类型里的期望行为。

为了来更好的理解,请参考如下代码:

function Vehicle(my) { 
var my = my || {}; 
my.speed = 0; 
my.running = false; this.speed = function() { 
return my.speed; 
}; 
this.start = function() { 
my.running = true; 
}; 
this.stop = function() { 
my.running = false; 
}; 
this.accelerate = function() { 
my.speed++; 
}; 
this.decelerate = function() { 
my.speed--; 
}, this.state = function() { 
if (!my.running) { 
return "parked"; 
} 
else if (my.running && my.speed) { 
return "moving"; 
} 
else if (my.running) { 
return "idle"; 
} 
}; 
}

上述代码我们定义了一个Vehicle函数,其构造函数为vehicle对象提供了一些基本的操作,我们来想想如果当前函数当前正运行在服务客户的产品环境上,如果现在需要添加一个新的构造函数来实现加快移动的vehicle。思考以后,我们写出了如下代码:
function FastVehicle(my) { 
var my = my || {}; var that = new Vehicle(my); 
that.accelerate = function() { 
my.speed += 3; 
}; 
return that; 
}

在浏览器的控制台我们都测试了,所有的功能都是我们的预期,没有问题,FastVehicle的速度增快了3倍,而且继承他的方法也是按照我们的预期工作。此后,我们开始部署这个新版本的类库到产品环境上,可是我们却接到了新的构造函数导致现有的代码不能支持执行了,下面的代码段揭示了这个问题:
var maneuver = function(vehicle) { 
write(vehicle.state()); 
vehicle.start(); 
write(vehicle.state()); 
vehicle.accelerate(); 
write(vehicle.state()); 
write(vehicle.speed()); 
vehicle.decelerate(); 
write(vehicle.speed()); 
if (vehicle.state() != "idle") { 
throw "The vehicle is still moving!"; 
} 
vehicle.stop(); 
write(vehicle.state()); 
};

根据上面的代码,我们看到抛出的异常是“The vehicle is still moving!”,这是因为写这段代码的作者一直认为加速(accelerate)和减速(decelerate)的数字是一样的。但FastVehicle的代码和Vehicle的代码并不是完全能够替换掉的。因此,FastVehicle违反了里氏替换原则。

在这点上,你可能会想:“但,客户端不能老假定vehicle都是按照这样的规则来做”,里氏替换原则(LSP)的妨碍(译者注:就是妨碍实现LSP的代码)不是基于我们所想的继承子类应该在行为里确保更新代码,而是这样的更新是否能在当前的期望中得到实现。

上述代码这个case,解决这个不兼容的问题需要在vehicle类库或者客户端调用代码上进行一点重新设计,或者两者都要改。

减少LSP妨碍
那么,我们如何避免LSP妨碍?不幸的话,并不是一直都是可以做到的。我们这里有几个策略我们处理这个事情。

契约(Contracts)
处理LSP过分妨碍的一个策略是使用契约,契约清单有2种形式:执行说明书(executable specifications)和错误处理,在执行说明书里,一个详细类库的契约也包括一组自动化测试,而错误处理是在代码里直接处理的,例如在前置条件,后置条件,常量检查等,可以从Bertrand Miller的大作《契约设计》中查看这个技术。虽然自动化测试和契约设计不在本篇文字的范围内,但当我们用的时候我还是推荐如下内容:

检查使用测试驱动开发(Test-Driven Development)来指导你代码的设计
设计可重用类库的时候可随意使用契约设计技术
对于你自己要维护和实现的代码,使用契约设计趋向于添加很多不必要的代码,如果你要控制输入,添加测试是非常有必要的,如果你是类库作者,使用契约设计,你要注意不正确的使用方法以及让你的用户使之作为一个测试工具。

避免继承
避免LSP妨碍的另外一个测试是:如果可能的话,尽量不用继承,在Gamma的大作《Design Patterns ? Elements of Reusable Object-Orineted Software》中,我们可以看到如下建议:

Favor object composition over class inheritance
尽量使用对象组合而不是类继承
复制代码
有些书里讨论了组合比继承好的唯一作用是静态类型,基于类的语言(例如,在运行时可以改变行为),与JavaScript相关的一个问题是耦合,当使用继承的时候,继承子类型和他们的基类型耦合在一起了,就是说及类型的改变会影响到继承子类型。组合倾向于对象更小化,更容易想静态和动态语言语言维护。

与行为有关,而不是继承
到现在,我们讨论了和继承上下文在内的里氏替换原则,指示出JavaScript的面向对象实。不过,里氏替换原则(LSP)的本质不是真的和继承有关,而是行为兼容性。JavaScript是一个动态语言,一个对象的契约行为不是对象的类型决定的,而是对象期望的功能决定的。里氏替换原则的初始构想是作为继承的一个原则指南,等价于对象设计中的隐式接口。

举例来说,让我们来看一下Robert C. Martin的大作《敏捷软件开发 原则、模式与实践》中的一个矩形类型:

矩形例子
考虑我们有一个程序用到下面这样的一个矩形对象:

var rectangle = { 
length: 0, 
width: 0 
}; 
[code] 
过后,程序有需要一个正方形,由于正方形就是一个长(length)和宽(width)都一样的特殊矩形,所以我们觉得创建一个正方形代替矩形。我们添加了length和width属性来匹配矩形的声明,但我们觉得使用属性的getters/setters一般我们可以让length和width保存同步,确保声明的是一个正方形: 
[code] 
var square = {}; 
(function() { 
var length = 0, width = 0; 
// 注意defineProperty方式是262-5版的新特性 
Object.defineProperty(square, "length", { 
get: function() { return length; }, 
set: function(value) { length = width = value; } 
}); 
Object.defineProperty(square, "width", { 
get: function() { return width; }, 
set: function(value) { length = width = value; } 
}); 
})();

不幸的是,当我们使用正方形代替矩形执行代码的时候发现了问题,其中一个计算矩形面积的方法如下:
var g = function(rectangle) { 
rectangle.length = 3; 
rectangle.width = 4; 
write(rectangle.length); 
write(rectangle.width); 
write(rectangle.length * rectangle.width); 
};

该方法在调用的时候,结果是16,而不是期望的12,我们的正方形square对象违反了LSP原则,square的长度和宽度属性暗示着并不是和矩形100%兼容,但我们并不总是这样明确的暗示。解决这个问题,我们可以重新设计一个shape对象来实现程序,依据多边形的概念,我们声明rectangle和square,relevant。不管怎么说,我们的目的是要说里氏替换原则并不只是继承,而是任何方法(其中的行为可以另外的行为)。

总结
里氏替换原则(LSP)表达的意思不是继承的关系,而是任何方法(只要该方法的行为能体会另外的行为就行)。

Javascript 相关文章推荐
动态样式类封装JS代码
Sep 02 Javascript
超级酷和最实用的jQuery实例收集(20个)
Apr 21 Javascript
屏蔽F1~F12的快捷键的js函数
May 06 Javascript
jQuery动态添加的元素绑定事件处理函数代码
Aug 02 Javascript
JavaScript中具名函数的多种调用方式总结
Nov 08 Javascript
分享几种比较简单实用的JavaScript tabel切换
Dec 31 Javascript
jQuery ui autocomplete选择列表被Bootstrap模态窗遮挡的完美解决方法
Sep 23 Javascript
原生js实现简单的Ripple按钮实例代码
Mar 24 Javascript
基于js中的存储键值对以及注意事项介绍
Mar 30 Javascript
vue better scroll 无法滚动的解决方法
Jun 07 Javascript
基于vue实现移动端圆形旋钮插件效果
Nov 28 Javascript
Angular(5.2->6.1)升级小结
Dec 27 Javascript
深入理解JavaScript系列(7) S.O.L.I.D五大原则之开闭原则OCP
Jan 15 #Javascript
深入理解JavaScript系列(6):S.O.L.I.D五大原则之单一职责SRP
Jan 15 #Javascript
深入理解JavaScript系列(6) 强大的原型和原型链
Jan 15 #Javascript
深入理解JavaScript系列(4) 立即调用的函数表达式
Jan 15 #Javascript
深入理解JavaScript系列(3) 全面解析Module模式
Jan 15 #Javascript
深入理解JavaScript系列(2) 揭秘命名函数表达式
Jan 15 #Javascript
深入理解JavaScript系列(1) 编写高质量JavaScript代码的基本要点
Jan 15 #Javascript
You might like
PHP正则提取不包含指定网址的图片地址的例子
2014/04/21 PHP
php版微信小店调用api示例代码
2016/11/12 PHP
PHP时间日期增减操作示例【date strtotime实现加一天、加一月等操作】
2018/12/21 PHP
imagettftext() 失效,不起作用
2021/03/09 PHP
百度地图自定义控件分享
2015/03/04 Javascript
jquery实现动静态条形统计图
2015/08/17 Javascript
分享15个大家都熟知的jquery小技巧
2015/12/02 Javascript
javascript实现图片左右滚动效果【可自动滚动,有左右按钮】
2016/09/19 Javascript
浅谈node中的exports与module.exports的关系
2017/08/01 Javascript
基于jquery的on和click的区别详解
2018/01/15 jQuery
js中的 || 与 && 运算符详解
2018/05/24 Javascript
微信小程序chooseImage的用法(从本地相册选择图片或使用相机拍照)
2018/08/22 Javascript
vue的keep-alive中使用EventBus的方法
2019/04/23 Javascript
微信小程序:数据存储、传值、取值详解
2019/05/07 Javascript
vue如何限制只能输入正负数及小数
2019/07/04 Javascript
eslint+prettier统一代码风格的实现方法
2020/07/22 Javascript
Nuxt的路由配置和参数传递方式
2020/11/06 Javascript
[03:20]2015国际邀请赛全明星表演赛
2015/08/08 DOTA
[01:10:24]DOTA2-DPC中国联赛 正赛 VG vs Aster BO3 第一场 2月28日
2021/03/11 DOTA
Python标准库之随机数 (math包、random包)介绍
2014/11/25 Python
Python多线程爬取豆瓣影评API接口
2019/10/22 Python
jupyter notebook 参数传递给shell命令行实例
2020/04/10 Python
Python ArgumentParse的subparser用法说明
2020/04/20 Python
python使用for...else跳出双层嵌套循环的方法实例
2020/05/17 Python
Django返回HTML文件的实现方法
2020/09/17 Python
总结python 三种常见的内存泄漏场景
2020/11/20 Python
可自定义箭头样式的CSS3气泡提示框
2016/03/16 HTML / CSS
html5响应式开发自动计算fontSize的方法
2020/01/13 HTML / CSS
电子商务毕业生求职信
2013/11/10 职场文书
科级干部考察材料
2014/02/15 职场文书
《胖乎乎的小手》教学反思
2014/02/26 职场文书
业务员的岗位职责
2014/03/15 职场文书
行政司机岗位职责
2015/04/10 职场文书
Win10系统下配置Java环境变量
2021/06/13 Java/Android
Python实现灰色关联分析与结果可视化的详细代码
2022/03/25 Python
MySQL索引 高效获取数据的数据结构
2022/05/02 MySQL