本篇是SOLID准则的第三局部,倡议先浏览前两局部:
JavaScript 中的 SOLID 准则:“S”代表什么
JavaScript 中的 SOLID 准则:“O”代表什么
这是SOLID的第三篇文章(原文一共五篇),作者是serhiirubets,欢送继续关注。
里氏替换准则(Liskov Substitution Principle)
L - 里氏替换准则。这个准则是指:如果S是T的子类型,那么程序中的T对象能够被S对象替换,不须要改变程序中任何所需属性。从定义上可能没有方法清晰的了解其含意,咱们略微换一个说法:应用指针或援用基类的函数必须能够替换为其派生类。// todo
让咱们用更简略的形式来形容它,例如:你有一个“Car”类,并且在不同中央进行了应用。这个准则的意思是:每一个应用Car类的中央,都应该能够被Car类的子类替换。如果咱们有一个继承自“Car“的“Passenger Car”, 或者有一个“SUV”类也继承自“Car“,如果咱们把“Car”类替换成“SUV”类或者“Passenger Car”类,即把父类Car替换成任何一个子类后,咱们的零碎应该像以前一样失常工作。
举个简略的例子,咱们有一个“Rectangle”(矩形)类,因为”正方形“也是“矩形”,咱们能够创立一个根本的“Rectangle”类和“Square”类,“Square”继承自“Rectangle”。
class Rectangle { constructor(width,height) { this.width = width this.height = height } setWidth(width) { this.width = width } setHeight(height) { this.height = height } getArea() { return this.width * this.height }}// Square计算面积的形式有点不同,它的高度和宽度一样的,重写setWidth和setHeight办法。class Square extends Rectangle { setWidth(width) { this.width = width; this.height = width; } setHeight(height) { this.width = height; this.height = height; }}
const rectangleFirst = new Rectangle(10, 15)const rectangleSecond = new Rectangle(5, 10)console.log(rectangleFirst.getArea()); // 150console.log(rectangleSecond.getArea()); // 50rectangleFirst.setWidth(20)rectangleSecond.setWidth(15)console.log(rectangleFirst.getArea()); // 300console.log(rectangleSecond.getArea()); // 150
咱们创立了两个实例,查看了矩形面积,更改宽高并再次查看了面积,咱们看到一切正常,代码按预期工作,然而,让咱们再看一下里氏替换准则:如果咱们更改任何子类的基类,咱们的零碎应该像以前一样工作。
const rectangleFirst = new Square(10, 15)const rectangleSecond = new Square(5, 10)console.log(rectangleFirst.getArea()); // 150console.log(rectangleSecond.getArea()); // 50rectangleFirst.setWidth(20)rectangleSecond.setWidth(15)console.log(rectangleFirst.getArea()); // 400console.log(rectangleSecond.getArea()); // 225
咱们把new Rectangle()
替换为new Square()
后发现,在setWidth
之后, getArea
返回了和替换之前不同的值,很显著咱们没有遵循里氏替换准则。
那么咱们应该怎么解决呢?解决方案是应用继承,但不是从”Rectangle“类,而是筹备一个更“正确”的类。比方,咱们创立一个“Sharp”类,它只负责计算面积:
class Shape { getArea() { return this.width * this.height; }}class Rectangle { constructor(width,height) { super(); this.width = width this.height = height } setWidth(width) { this.width = width } setHeight(height) { this.height = height }}class Square extends Shape { setWidth(width) { this.width = width; this.height = width; } setHeight(height) { this.width = height; this.height = height; }}
咱们创立了一个更通用的基类Shape
,在应用new Shape()
的中央咱们都能够把Shape
批改为任何它的子类,而不会毁坏原有逻辑。
在咱们的示例中,Rectangle和Square是不同的对象,它们蕴含了一些类似的逻辑,但也有不同的逻辑,所以把他们离开而不是用作“父子”类会更正确。
咱们再来看一个对了解这个准则有帮忙的例子:
咱们要创立一个Bird
类,咱们正在思考应该增加什么办法,从第一个角度来看,咱们能够思考增加fly
办法,因为所有的鸟都会飞。
class Bird{ fly(){}}function allFly(birds) { birds.forEach(bird => bird.fly())}allFly([new Bird(), new Bird(), new Bird()])
之后,咱们意识到存在不同的鸟类:鸭子、鹦鹉、天鹅。
class Duck extends Bird { quack(){}}class Parrot extends Bird { repeat(){}}class Swan extends Bird{ beBeautiful(){}}
当初,里氏替换准则说,如果咱们把基类更改为子类,零碎应该像以前一样工作:
class Duck extends Bird { quack(){}}class Parrot extends Bird { repeat(){}}class Swan extends Bird{ beBeautiful(){}}function allFly(birds){ birds.forEach(bird=> bird.fly())}allFly([new Duck(), new Parrot(), new Swan()])
咱们在调用allFly
函数时,扭转了参数,咱们调用了new Duck()
,new Parrot()
,new Swan()
, 而不是调用new Bird()
。一切正常,咱们正确的遵循了里氏替换准则。
当初咱们想再增加一只企鹅,然而企鹅并不会飞,如果想调用fly
办法,咱们就抛出一个谬误。
class Penguin extends Bird { fly(){ throw new Error('Sorry, but I cannot fly') } swim(){}}allFly([new Duck(), new Parrot(), new Swan(), new Penguin()])
然而咱们遇到一个问题:fly办法并不冀望呈现外部谬误,allFly办法也只是为会飞的鸟创立的,企鹅不会飞,所以咱们违反了里氏替换准则。
怎么解决这个问题?与其创立一个根本的Bird
类,不如创立一个FlyingBird
类,所有会飞的鸟都只继承自FlyingBird
类,allFly办法也只承受Flying Bird
。
class Bird{}class FlyingBird{ fly(){}}class Duck extends FlyingBird { quack(){}}class Parrot extends FlyingBird { repeat(){}}class Swan extends FlyingBird{ beBeautiful(){}}class Penguin extends Bird { swim(){}}
Penguin
继承自Bird类,而不是FlyingBird类,咱们也不须要调用会引发谬误的fly办法。在任何调用FlyingBird的中央,能够间接换成更具体的鸟类,比方Duck、Parrot、Swan,代码也会失常工作。
心愿你能够通过本文可能更好的了解里氏替换准则,理解在JavaScript是如何工作的和如何在我的项目中应用。下一篇中,咱们将持续学习SOLID中的下一个'L'字母
欢送关注微信公众号”混沌前端“
举荐浏览:
基于TypeScript了解程序设计的SOLID准则
clean-code-javascript: SOLID