里氏替换原则(LSP)指出派生类必须可以替代其基类。面向对象编程中的继承允许你通过子类添加功能。然而,若操作不当,可能导致不必要的复杂性。
SOLID的第三支柱告诉你如何应用继承使你的子类更健壮和灵活。
想象你的游戏需要一个名为Vehicle的类。这将是你将为应用程序创建的车辆子类的基类。例如,你可能需要一辆汽车或卡车。
一切从Vehicle继承。
在你可以使用基类(Vehicle)的任何地方,你应该能够使用子类(如Car或Truck)而不破坏应用程序。
你的Vehicle类可能看起来像这样:
public class Vehicle
{
public float speed = 100;
public Vector3 direction;
public void GoForward() { ... }
public void Reverse() { ... }
public void TurnRight() { ... }
public void TurnLeft() { ... }
}
假设你正在构建一个回合制游戏,你在板上移动车辆。

汽车对火车的示例游戏
你可以有另一个名为Navigator的类来沿着预定路径驾驶车辆:
public class Navigator
{
public void Move(Vehicle vehicle)
{
vehicle.GoForward();
vehicle.TurnLeft();
vehicle.GoForward();
vehicle.TurnRight();
vehicle.GoForward();
}
}
使用这个类,你期望能够将任何车辆传递给Navigator的Move方法,这对汽车和卡车很有效。但是,当你想实现一个名为Train的类时会发生什么?

火车会违反你的基类。
TurnLeft和TurnRight方法在Train类中不起作用,因为火车不能离开轨道。如果你确实将火车传递给Navigator的Move方法,当你到达那些行时,它会抛出未实现的异常(或什么都不做)。如果你不能用一个类型替换其子类型,你就违反了里氏替换原则。
由于Train是Vehicle的子类型,你期望在接受Vehicle的任何地方使用它。否则可能会使你的代码行为不可预测。
考虑一些更严格遵守里氏替换原则的技巧:
- 如果你在子类删除某个功能,你可能违反里氏替换:NotImplementedException是一个明显的信号,表明你违反了这个原则。留空一个方法也是如此。如果子类不像基类那样行事,你就没有遵循LSP------即使没有明确的错误或异常。
- 保持抽象简单:你放入基类的逻辑越多,你越有可能违反LSP。基类应该只表达派生子类的共同功能。
- 子类需要具有与基类相同的公共成员:这些成员在调用时也需要具有相同的特性和行为。
- 在建立类层次结构之前考虑类API:即使你将它们都视为车辆,Car和Train继承自单独的父类可能更有意义。现实中的分类并不总是转化为类层次结构。
- 优先考虑组合而不是继承:不要试图通过继承传递功能,创建一个接口或单独的类来封装特定行为。然后通过混合和匹配不同的功能来建立"组合"。

优先考虑组合而不是继承
要修复这个设计,废弃原始的Vehicle类型,然后将大部分功能移入接口:
public interface ITurnable
{
public void TurnRight();
public void TurnLeft();
}
public interface IMovable
{
public void GoForward();
public void Reverse();
}
通过更严格地遵循LSP,创建一个RoadVehicle类型和RailVehicle类型。然后Car和Train将各自继承自相应的基类。

考虑Liskov替换进行重构
public class RoadVehicle : IMovable, ITurnable
{
public float speed = 100f;
public float turnSpeed = 5f;
public virtual void GoForward() { ... }
public virtual void Reverse() { ... }
public virtual void TurnLeft() { ... }
public virtual void TurnRight() { ... }
}
public class RailVehicle : IMovable
{
public float speed = 100;
public virtual void GoForward() { ... }
public virtual void Reverse() { ... }
}
public class Car : RoadVehicle { ... }
public class Train : RailVehicle { ... }
这样,功能通过接口而不是继承来实现。Car和Train不再共享相同的基类,现在满足了LSP。虽然你可以从同一个基类派生RoadVehicle和RailVehicle,但在这种情况下没有必要。
这种思维方式可能违反直觉,因为你对现实世界有某些假设。在软件开发中,这被称为圆-椭圆问题。并不是每个实际的"是"关系都转化为继承。记住,你希望你的软件设计驱动你的类层次结构,而不是你对现实的经验知识。
遵循里氏替换原则来限制你使用继承的方式,以保持你的代码库可扩展和灵活。