深入理解JavaScript系列(8) S.O.L.I.D五大原则之里氏替换原则LSP
本文深入解析JavaScript中S.O.L.I.D五大原则之一的里氏替换原则(LSP)。通过Vehicle和Square/Rectangle两个典型案例,阐述子类替换父类时行为一致性的重要性,并探讨契约设计、对象组合及行为兼容性等避免LSP违背的策略。
前言
里氏替换原则(LSP)概述
本章我们将深入探讨S.O.L.I.D五大原则在JavaScript语言中的具体实现,重点讲解第三部分:里氏替换原则(Liskov Substitution Principle,简称LSP)。
英文原文参考: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";
}
};
} FastVehicle案例:违反LSP的典型场景
上述代码定义了一个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的代码)并非基于我们主观认为继承子类应在行为上确保更新代码,而是这种更新是否能在当前期望中得以实现。
针对上述案例,解决不兼容问题需要对Vehicle类库或客户端调用代码进行重新设计,或者两者都需要修改。
减少LSP妨碍的策略
减少LSP妨碍
那么,我们如何避免LSP妨碍?不幸的是,这并非总是可行的。我们有几种策略来处理这种情况。
契约(Contracts)
处理LSP过度妨碍的一个策略是使用契约。契约清单有两种形式:执行说明书(executable specifications)和错误处理。在执行说明书中,详细类库的契约包括一组自动化测试;而错误处理则在代码中直接处理,例如前置条件、后置条件、常量检查等。有关此技术,可参考Bertrand Meyer的大作《契约设计》(Design by Contract)。虽然自动化测试和契约设计不在本文讨论范围内,但推荐使用以下内容:
检查使用测试驱动开发(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)表达的意思不是继承的关系,而是任何方法(只要该方法的行为能体会另外的行为就行)。
JS split切割后出现空字符串怎么过滤
2026-11-05 16:00:30
在 React 或 Vue 项目中封装使用 JS 节流防抖
2026-11-05 15:27:29
JavaScript中怎么将带小数的字符串转为整数?
2026-11-05 14:32:38
怎么用JavaScript策略模式重构电商促销计算逻辑?
2026-11-05 14:01:09
JS使用constructor判断数据类型可靠吗
2026-11-05 13:49:41
