我正在试图弄清楚Liskov的替换原理,而矩形和正方形的例子并没有真正打动我。所以矩形和方形的例子是,如果你有一个基矩形类和一个从它继承的正方形类,square类的setWidth/setHeight方法的实现方式不同,因此当它调用setWidth时,它也会将其高度更改为与width相同。
但我不明白为什么这是个问题。你肯定希望这个正方形有同样的宽度/高度吧?无论如何,我想知道这是否适用于我下面的车辆/汽车/飞机结构。
TLDR:我有一辆车和一架飞机,它们继承了Vehicle类的抽象move()方法。汽车增加了它的x和y位置,平面增加了x、y和z位置。如果我有一个函数接收一个Vehicle类并调用move,这是否打破了liskov的替换原理?如果是的话,为什么设计不好?
我有一个抽象的车辆类
abstract class Vehicle {
private wheels;
private make;
private seats;
abstract honk(){};
abstract move(){};
}
然后我有飞机和汽车的两个子类
汽车
class Car extends Vehicle {
private location: Location2d;
public honk() {
console.log(this.getMake() + " IS HONKING");
}
public move(){
this.location.x++;
this.location.y++
}
}
export default Car;
平面
class Plane extends Vehicle implements FlyingVehicle {
private maxAltitude;
private location: Location3d;
public honk() {
console.log(this.getMake() + " is HONKING")
}
public move(){
this.location.x++;
this.location.y++;
this.location.z++;
}
}
现在,如果我有一个函数,它遍历一组车辆,然后调用所有车辆上的move。这是否违反了利斯科夫的替代原则?
我从未想过正方形/三角形/汽车/自行车是OOP的好例子。从来没有在真实的代码中看到过这些,这让争论这些东西变得有点困难,因为你需要以一种保持隐喻的方式来做这件事。
飞机和汽车很少被同时使用这两种代码的代码控制;)
不过,替代原则主要是关于类型的。因此,任何子类的方法都应该可以由可以调用主类的东西调用,并且返回的类型应该与从父类返回的类型兼容。
它其实并不关心副作用。你可以实现一个新的子类,它根本不移动车辆,而是做一些完全无关的事情。只要类型有意义,它就是有效的。
现在我认为,由任一方实现的move
方法可能应该做一些符合原始方法精神的事情,但这更多的是关于一般合理的设计,而不是特别的liskov。
现在如果我有一个函数,它遍历一组车辆,然后对所有车辆调用move。这是否违反了利斯科夫的替代原则?
不,这不会打破利斯科夫替代原则。(当然,这并不意味着它一定是好代码。)
我认为您缺少的关键见解是LSP是关于方法的接口——它的参数和返回值。子类必须接受父类接受的任何参数集,并且可以接收父类拒绝的值。子类必须返回父类可以返回的值,但可能不返回父类可能返回的所有可能值。
该方法的内部结构可能会发生变化,可能会产生不同的副作用。
这里有一个例子,希望能解释为什么这很重要。
有一个人力资源软件系统。它有一个员工类,该员工类上的一种方法是
calculatePaycheque(month: int)
,它返回员工当月应支付的金额,类型为int
。如果CCD_ 4是<1或>12.工资是用工资除以12来计算的。这种方法在一些不同的地方被称为,既可以预测某个月的工资支出总额,也可以在每个月底实际发放工资。有些员工是季节性工人,所以他们在冬天不工作,也拿不到工资。然而,他们会根据工作月份的表现获得奖金。为此,创建了一个新类"seasonalEmployee"。它覆盖了
calculatePaycheque()
方法,因此在3月至9月,他们可以获得年薪的1/12,还可以增加奖金。如果我们不遵守LSP,那么我们可以返回一个对象
{ fixedAmount: int, bonus: int }
。由于工人只工作7个月,我们可以取month: int
,但如果它<3或>9.假设我们这样做,然后我们将
seasonalEmployee
添加到employee
对象的大列表中,这些对象被处理以预测9月份的工资单。如果我们不写一些额外的代码来检查响应的类型并对其进行数学运算,那么将每位员工的工资支票相加的代码将无法将我们的{fixedAmount: int, bonus: int}
添加到int
值的列表中。当我们计算10月份的工资单时,seasonalEmployee
将出错,因为该员工当月没有工资。因此,现在我们需要一名警卫,防止我们在季节性员工的季节外拨打calculatePaycheque()
。如果我们遵循利斯科夫替代原则,我们就可以省去所有这些头痛的事情。在这种情况下,我们仍然采用一个可以在1-12范围内的参数
month: int
,并且我们仍然返回一个类型为int
的单个数量。这样,当我们开始向系统添加seasonalEmployee
对象时,就不需要更新代码库的其余部分。
该方法的内部工作方式发生了变化。但是,该方法本身仍然可以与父方法互换。这意味着我们可以只处理新代码,而不需要重构或扩展代码库的其余部分来处理更改。
Square
/Rectangle
与setWidth
/setHeight
的示例应该说明继承概念的问题。假设
- 基类
Rectangle
提供setWidth
和setHeight
方法,它们应该只更改相应的属性,并且 - 子类
Square
具有一个不变量,即宽度总是等于高度
给定这两个要求,您必须违反Liskov替换原则(通过违反setWidth
和setHeight
方法的约定),否则您必须违反子类的不变量(通过允许宽度与高度不同)。
但是,如果Rectangle
类的约定没有声明setWidth
必须仅更改宽度(同样,setHeight
必须只改变高度),那么Square
类可以在不违反基类约定的情况下覆盖这些方法,一切都很好。这更像您的Car
和Plane
示例;Vehicle
基类可能没有声明move
方法不能改变z
位置的约定,因此Plane.move
不违反这样的约定。在这种情况下,没有违反利斯科夫替代原则。