JavaScript 中的 SOLID 原则(三):“L”代表什么

简介: JavaScript 中的 SOLID 原则:“L”代表什么

本篇是SOLID原则的第三部分,建议先阅读前两部分:

JavaScript 中的 SOLID 原则:“S”代表什么

JavaScript 中的 SOLID 原则:“O”代表什么

这是SOLID的第三篇文章(原文一共五篇),作者是serhiirubets,欢迎持续关注。

里氏替换原则(Liskov Substitution Principle)

L - 里氏替换原则。这个原则是指:如果S是T的子类型,那么程序中的T对象可以被S对象替换,不需要改变程序中任何所需属性。从定义上可能没有办法清晰的理解其含义,我们稍微换一个说法:函数使用的指针或引用基类必须可以替换为其派生类。

让我们用更简单的方式来描述它,例如:你有一个“Car”类,并且在不同地方进行了使用。这个原则的意思是:每一个使用Car类的地方,都应该可以被Car类的子类替换。如果我们有一个继承自“Car“的“Passenger Car”, 或者有一个“SUV”类也继承自“Car“,如果我们把“Car”类替换成“SUV”类或者“Passenger Car”类,即把父类Car替换成任何一个子类后,我们的系统应该像以前一样正常工作。

举个简单的例子,我们有一个“Rectangle”(矩形)类,因为”正方形“也是“矩形”,我们可以创建一个基本的“Rectangle”类和“Square”类,“Square”继承自“Rectangle”。

classRectangle {

 constructor(width,height) {

   this.width=width

   this.height=height

 }

 setWidth(width) {

   this.width=width

 }

 setHeight(height) {

   this.height=height

 }

 getArea() {

   returnthis.width*this.height

 }

}

// Square计算面积的方式有点不同,它的高度和宽度一样的,重写setWidth和setHeight方法。

classSquareextendsRectangle {

 setWidth(width) {

   this.width=width;

   this.height=width;

 }

 setHeight(height) {

   this.width=height;

   this.height=height;

 }

}

constrectangleFirst=newRectangle(10, 15)

constrectangleSecond=newRectangle(5, 10)

console.log(rectangleFirst.getArea()); // 150

console.log(rectangleSecond.getArea()); // 50

rectangleFirst.setWidth(20)

rectangleSecond.setWidth(15)

console.log(rectangleFirst.getArea()); // 300

console.log(rectangleSecond.getArea()); // 150

我们创建了两个实例,查看了矩形面积,更改宽高并再次检查了面积,我们看到一切正常,代码按预期工作,但是,让我们再看一下里氏替换原则:如果我们更改任何子类的基类,我们的系统应该像以前一样工作。

constrectangleFirst=newSquare(10, 15)

constrectangleSecond=newSquare(5, 10)

console.log(rectangleFirst.getArea()); // 150

console.log(rectangleSecond.getArea()); // 50

rectangleFirst.setWidth(20)

rectangleSecond.setWidth(15)

console.log(rectangleFirst.getArea()); // 400

console.log(rectangleSecond.getArea()); // 225

我们把new Rectangle() 替换为new Square()后发现,在setWidth之后, getArea返回了和替换之前不同的值,很明显我们打破了里氏替换原则。

那么我们应该怎么解决呢?解决方案是使用继承,但不是从”Rectangle“类,而是准备一个更“正确”的类。比如,我们创建一个“Sharp”类,它只负责计算面积:

classShape {

 getArea() {

   returnthis.width*this.height;

 }

}

classRectangle {

 constructor(width,height) {

   super();

   this.width=width

   this.height=height

 }

 setWidth(width) {

   this.width=width

 }

 setHeight(height) {

   this.height=height

 }

}

classSquareextendsShape {

 setWidth(width) {

   this.width=width;

   this.height=width;

 }

 setHeight(height) {

   this.width=height;

   this.height=height;

 }

}

我们创建了一个更通用的基类Shape,在使用new Shape()的地方我们都可以把Shape修改为任何它的子类,而不会破坏原有逻辑。

在我们的示例中,Rectangle和Square是不同的对象,它们包含了一些相似的逻辑,但也有不同的逻辑,所以把他们分开而不是用作“父子”类会更正确。

我们再来看一个对理解这个原则有帮助的例子:

我们要创建一个Bird类,我们正在考虑应该添加什么方法,从第一个角度来看,我们可以考虑添加fly方法,因为所有的鸟都会飞。

classBird{

 fly(){}

}

functionallFly(birds) {

 birds.forEach(bird=>bird.fly())

}

allFly([newBird(), newBird(), newBird()])

之后,我们意识到存在不同的鸟类:鸭子、鹦鹉、天鹅。

classDuckextendsBird {

 quack(){}

}

classParrotextendsBird {

 repeat(){}

}

classSwanextendsBird{

 beBeautiful(){}

}

现在,里氏替换原则说,如果我们把基类更改为子类,系统应该像以前一样工作:

classDuckextendsBird {

 quack(){}

}

classParrotextendsBird {

 repeat(){}

}

classSwanextendsBird{

 beBeautiful(){}

}

functionallFly(birds){

 birds.forEach(bird=>bird.fly())

}

allFly([newDuck(), newParrot(), newSwan()])

我们在调用allFly函数时,改变了参数,我们调用了new Duck(),new Parrot(),new Swan(), 而不是调用new Bird()。一切正常,我们正确的遵循了里氏替换原则。

现在我们想再添加一只企鹅,但是企鹅并不会飞,如果想调用fly方法,我们就抛出一个错误。

classPenguinextendsBird {

 fly(){

   thrownewError('Sorry, but I cannot fly')

 }

 swim(){}

}

allFly([newDuck(), newParrot(), newSwan(), newPenguin()])

但是我们遇到一个问题:fly方法并不期望出现内部错误,allFly方法也只是为会飞的鸟创建的,企鹅不会飞,所以我们违背了里氏替换原则。

怎么解决这个问题?与其创建一个基本的Bird类,不如创建一个FlyingBird类,所有会飞的鸟都只继承自FlyingBird类,allFly方法也只接受Flying Bird

classBird{

}

classFlyingBird{

 fly(){}

}

classDuckextendsFlyingBird {

 quack(){}

}

classParrotextendsFlyingBird {

 repeat(){}

}

classSwanextendsFlyingBird{

 beBeautiful(){}

}

classPenguinextendsBird {

 swim(){}

}

Penguin继承自Bird类,而不是FlyingBird类,我们也不需要调用会引发错误的fly方法。在任何调用FlyingBird的地方,可以直接换成更具体的鸟类,比如Duck、Parrot、Swan,代码也会正常工作。

希望你可以通过本文能够更好的理解里氏替换原则,了解在JavaScript是如何工作的和如何在项目中使用。下一篇中,我们将继续学习SOLID中的下一个'L'字母

欢迎关注微信公众号”混沌前端“

推荐阅读:

基于TypeScript理解程序设计的SOLID原则

clean-code-javascript: SOLID


相关文章
|
设计模式 缓存 JavaScript
你不知道的javascript设计模式(十七) ----编程设计原则和设计规则
你不知道的javascript设计模式(十七) ----编程设计原则和设计规则
105 0
|
存储 SQL 缓存
JavaScript 中的 SOLID 原则(五):“D”代表什么
JavaScript 中的 SOLID 原则(五):“D”代表什么
|
JavaScript 前端开发
JavaScript 中的 SOLID 原则(四):“I”代表什么
JavaScript 中的 SOLID 原则(四):“I”代表什么
|
设计模式 前端开发 JavaScript
JavaScript 中的 SOLID 原则(二):“O”代表什么
JavaScript 中的 SOLID 原则:“O”代表什么
|
设计模式 JavaScript 前端开发
JavaScript 中的 SOLID 原则(一):“S”代表什么
JavaScript 中的 SOLID 原则:“S”代表什么
|
5月前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的客户关系管理系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的客户关系管理系统附带文章源码部署视频讲解等
100 2
|
5月前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的小区物流配送系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的小区物流配送系统附带文章源码部署视频讲解等
134 4
|
5月前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的宠物援助平台附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的宠物援助平台附带文章源码部署视频讲解等
85 4
|
5月前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的宠物交易平台附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的宠物交易平台附带文章源码部署视频讲解等
76 4