深入理解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 相关文章推荐
jquery统计复选框选中示例
Nov 05 Javascript
Jquery.Form 异步提交表单的简单实例
Mar 03 Javascript
浅析js中substring和substr的方法
Nov 09 Javascript
jQuery中hover与mouseover和mouseout的区别分析
Dec 24 Javascript
JavaScript设计模式开发中组合模式的使用教程
May 18 Javascript
基于JavaScript实现鼠标箭头移动图片跟着移动
Aug 30 Javascript
Javascript中常见的逻辑题和解决方法
Sep 17 Javascript
3种不同的ContextMenu右键菜单实现代码
Nov 03 Javascript
webpack4.0打包优化策略整理小结
Mar 30 Javascript
vue使用Element组件时v-for循环里的表单项验证方法
Jun 28 Javascript
Javascript 实现 Excel 导入生成图表功能
Oct 22 Javascript
js实现双色球效果
Aug 02 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之第七天
2006/10/09 PHP
PHP得到mssql的存储过程的输出参数功能实现
2012/11/23 PHP
php字符串替换函数substr_replace()用法实例
2015/03/17 PHP
thinkPHP框架实现多表查询的方法
2018/06/14 PHP
laravel ORM关联关系中的 with和whereHas用法
2019/10/16 PHP
jqPlot jquery的页面图表绘制工具
2009/07/25 Javascript
Javascript 拖拽的一些高级的应用(逐行分析代码,让你轻松了拖拽的原理)
2015/01/23 Javascript
js鼠标点击图片实现随机变换图片的方法
2015/02/16 Javascript
JS修改iframe页面背景颜色的方法
2015/04/01 Javascript
js实现Select头像选择实时预览代码
2015/08/17 Javascript
基于jQuery实现仿百度首页选项卡切换效果
2016/05/29 Javascript
jquery实现的回旋滚动效果完整实例【附demo源码下载】
2016/09/20 Javascript
Javascript中作用域的详细介绍
2016/10/06 Javascript
JS中用三种方式实现导航菜单中的二级下拉菜单
2016/10/31 Javascript
微信小程序(六):列表上拉加载下拉刷新示例
2017/01/13 Javascript
Bootstrap table使用方法汇总
2017/11/17 Javascript
对node通过fs模块判断文件是否是文件夹的实例讲解
2019/06/10 Javascript
JS合并两个数组的3种方法详解
2019/10/24 Javascript
vue+vuex+axios从后台获取数据存入vuex,组件之间共享数据操作
2020/07/31 Javascript
react项目从新建到部署的实现示例
2021/02/19 Javascript
Python实现的一个找零钱的小程序代码分享
2014/08/25 Python
用python实现面向对像的ASP程序实例
2014/11/10 Python
详解Python编程中基本的数学计算使用
2016/02/04 Python
python实现支付宝转账接口
2019/05/07 Python
详细介绍pandas的DataFrame的append方法使用
2019/07/31 Python
Python+MySQL随机试卷及答案生成程序的示例代码
2021/02/01 Python
巴西家用小家电购物网站:Polishop
2016/08/07 全球购物
美国顶级品牌男士大码服装店:DXL
2017/08/30 全球购物
毕业生找工作的自我评价
2013/10/18 职场文书
给领导的致歉信范文
2014/01/13 职场文书
2014年民主评议党员个人总结
2014/09/24 职场文书
交通事故和解协议书
2015/01/27 职场文书
重阳节简报
2015/07/20 职场文书
使用Ajax实现无刷新上传文件
2022/04/12 Javascript
PostgreSQL基于pgrouting的路径规划处理方法
2022/04/18 PostgreSQL
Docker容器harbor私有仓库部署和管理
2022/08/05 Servers