关于里氏替换原则的疑问:Car与Plane的move实现是否违反该原则?
一、矩形与正方形示例的困惑
我正在理解里氏替换原则(Liskov's Substitution Principle),但矩形与正方形的示例让我困惑。该示例中,Square类继承Rectangle类后,setWidth/setHeight方法会同时修改宽高使其相等,我不解这为何是问题。
里氏替换原则的核心是子类必须能完全替换父类,且不改变原有程序的正确性。拿矩形和正方形来说,父类Rectangle有一个隐含的契约:调用setWidth只会修改宽度,高度保持不变;调用setHeight只会修改高度,宽度保持不变。
但Square继承后直接破坏了这个契约:调用setWidth(5)会同时把高度也改成5,这会导致原本依赖Rectangle契约的代码逻辑出错。比如有这样一段代码:
void stretchRectangle(Rectangle r) { r.setWidth(10); r.setHeight(20); assert r.getWidth() == 10 && r.getHeight() == 20; }
如果传入Square实例,这个断言会直接失败——因为setWidth之后高度变成10,再调用setHeight又把宽度改成20,最终宽高都是20,完全不符合原代码对Rectangle的预期。这就是违反LSP的典型表现:子类篡改了父类的行为契约,导致依赖父类的逻辑无法正常工作。
二、Vehicle、Car、Plane类结构的LSP合规性疑问
我有如下类结构,想请教:当编写函数遍历Vehicle数组并调用move方法时,是否违反里氏替换原则?若是,为何属于不良设计?
抽象类Vehicle代码:
abstract class Vehicle { private wheels; private make; private seats; abstract honk(); abstract move(); }
Car类代码:
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;
Plane类代码:
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++; } }
从当前代码来看,并没有违反里氏替换原则,但存在潜在的设计隐患:
当前合规的原因:
抽象类Vehicle定义的move方法只有一个宽泛的契约——"让交通工具完成移动动作",没有对移动的维度、坐标变化做具体约束。Car的2D移动和Plane的3D移动,都是对"移动"这个抽象行为的合理实现,没有破坏父类的契约。遍历Vehicle数组调用move时,只要代码不依赖"移动后z坐标保持不变"这类未在Vehicle中声明的隐含假设,程序就能正常运行,子类完全可以替换父类。潜在的不良设计风险:
问题出在Vehicle的抽象粒度不够精准。如果后续出现依赖"地面移动(z坐标不变)"的逻辑,比如一个parkVehicle(Vehicle v)函数,假设传入的Vehicle移动后不会离开地面,这时传入Plane就会引发问题——Plane的move会修改z坐标,违反了该函数对Vehicle的隐含预期。更合理的设计应该是拆分抽象:比如创建
GroundVehicle和AirVehicle两个抽象类,分别对应2D移动和3D移动的明确契约,Car继承GroundVehicle,Plane继承AirVehicle。这样能清晰区分不同移动类型的交通工具,避免后续代码因隐含假设而出现逻辑错误。
内容的提问来源于stack exchange,提问作者james

