41

Liskov 替换原则指出子类型应该可以替换该类型(不改变程序的正确性)。

  • 有人可以在车辆(汽车)领域提供这个原则的例子吗?
  • 有人可以提供一个在车辆领域违反这一原则的例子吗?

我已经阅读了方形/矩形示例,但我认为车辆示例将使我更好地理解这个概念。

4

5 回答 5

66

对我来说,鲍勃叔叔( Robert C Martin ) 的1996 年引述对LSP 的总结最好:

使用指向基类的指针或引用的函数必须能够在不知情的情况下使用派生类的对象。

最近,作为基于(通常是抽象的)基类/超类的子类的继承抽象的替代方案,我们也经常使用接口进行多态抽象。LSP 对消费者和抽象的实现都有影响:

  • 任何使用类或接口抽象的代码都必须在定义的抽象之外对类进行任何假设;
  • 超类的任何子类化或抽象的实现都必须遵守抽象接口的要求和约定。

LSP 合规性

这是一个使用IVehicle可以具有多个实现的接口的示例(或者,您可以将接口替换为具有多个子类的抽象基类 - 效果相同)。

interface IVehicle
{
   void Drive(int miles);
   void FillUpWithFuel();
   int FuelRemaining {get; } // C# syntax for a readable property
}

这个消费者的实现IVehicle保持在 LSP 的范围内:

void MethodWhichUsesIVehicle(IVehicle aVehicle)
{
   ...
   // Knows only about the interface. Any IVehicle is supported
   aVehicle.Drive(50);
 }

明显违规 - 运行时类型切换

这是一个违反 LSP 的示例,使用 RTTI 然后向下转换 - Bob 叔叔称之为“明显违反”:

void MethodWhichViolatesLSP(IVehicle aVehicle)
{
   if (aVehicle is Car)
   {
      var car = aVehicle as Car;
      // Do something special for car - this method is not on the IVehicle interface
      car.ChangeGear();
    }
    // etc.
 }

违规方法超出了约定的IVehicle接口,并为接口的已知实现(或子类,如果使用继承而不是接口)破解了特定路径。Bob 叔叔还解释说,使用类型转换行为的 LSP 违规通常也违反了Open and Closed 原则,因为需要不断修改函数以适应新的子类。

违反 - 前置条件被一个子类型强化

另一个违规示例是“子类型加强了先决条件”

public abstract class Vehicle
{
    public virtual void Drive(int miles)
    {
        Assert(miles > 0 && miles < 300); // Consumers see this as the contract
    }
 }

 public class Scooter : Vehicle
 {
     public override void Drive(int miles)
     {
         Assert(miles > 0 && miles < 50); // ** Violation
         base.Drive(miles);
     }
 }

在这里,Scooter 子类试图违反 LSP,因为它试图加强(进一步约束)基类Drive方法的先决条件 that miles < 300,现在最大小于 50 英里。这是无效的,因为根据合同定义Vehicle允许 300 英里。

类似地,后置条件可能不会被子类型削弱(即放松)。

(C#中Code Contracts的用户会注意,前置条件和后置条件必须通过类放在接口ContractClassFor上,不能放在实现类中,从而避免违规)

Subtle Violation - 子类滥用接口实现

违规(也是鲍勃叔叔的more subtle术语)可以用实现接口的可疑派生类来显示:

class ToyCar : IVehicle
{
    public void Drive(int miles) { /* Show flashy lights, make random sounds */ }
    public void FillUpWithFuel() {/* Again, more silly lights and noises*/}
    public int FuelRemaining {get {return 0;}}
}

在这里,无论驱动多远ToyCar,剩余燃料将始终为零,这会让IVehicle界面用户感到惊讶(即无限 MPG 消耗 - 永动机?)。在这种情况下,问题是尽管ToyCar已经实现了接口的所有要求,但ToyCar本质上并不是一个真实的接口,IVehicle只是“橡皮图章”。

防止您的接口或抽象基类以这种方式被滥用的一种方法是确保在接口/抽象基类上提供一组良好的单元测试,以测试所有实现是否符合预期(和任何假设)。单元测试也很擅长记录典型用法。例如,这NUnit Theory将拒绝ToyCar将其纳入您的生产代码库:

[Theory]
void EnsureThatIVehicleConsumesFuelWhenDriven(IVehicle vehicle)
{
    vehicle.FillUpWithFuel();
    Assert.IsTrue(vehicle.FuelRemaining > 0);
    int fuelBeforeDrive = vehicle.FuelRemaining;
    vehicle.Drive(20); // Fuel consumption is expected.
    Assert.IsTrue(vehicle.FuelRemaining < fuelBeforeDrive);
}

编辑,回复:OpenDoor

打开门听起来完全是一个不同的问题,因此需要相应地分开(即 SOLID 中的“S”“I”),例如

添加一个单独的 interface IDoor,然后车辆喜欢CarandTruck将实现IVehicleIDoor接口,但ScooterandMotorcycle只会实现IVehicle.

在所有情况下,为避免违反 LSP,需要这些接口对象的代码不应向下转换接口以访问额外功能。代码应该选择它需要的适当的最小接口/(超)类,并只使用该接口上的约定功能。

于 2013-12-31T17:40:57.570 回答
25

Image 我搬家时想租车。我打电话给租赁公司,问他们有什么型号。他们告诉我,我只会得到下一辆车:

public class CarHireService {
    public Car hireCar() {
        return availableCarPool.getNextCar();
    }
}

但是他们给了我一本小册子,告诉我他们所有的模型都具有以下功能:

public interface Car {
    public void drive();
    public void playRadio();
    public void addLuggage();
}

这听起来正是我正在寻找的,所以我预订了一辆车并高兴地离开。搬家那天,一辆一级方程式赛车出现在我家门外:

public class FormulaOneCar implements Car {
    public void drive() {
        //Code to make it go super fast
    }

    public void addLuggage() {
        throw new NotSupportedException("No room to carry luggage, sorry."); 
    }

    public void playRadio() {
        throw new NotSupportedException("Too heavy, none included."); 
    }
}

我不高兴,因为我基本上被他们的宣传册骗了——一级方程式赛车的假后备箱看起来可以装行李但打不开也没关系,这对搬家没用!

如果有人告诉我“这些是我们所有汽车都会做的事情”,那么任何给我的汽车都应该以这种方式运行。如果我不能相信他们宣传册中的细节,那就没用了。这就是里氏替换原则的精髓。

于 2014-01-09T09:38:45.820 回答
3

Liskov 替换原则指出,具有特定接口的对象可以被实现相同接口的不同对象替换,同时保留原始程序的所有正确性。这意味着不仅接口必须具有完全相同的类型,而且行为也必须保持正确。

在车辆中,您应该能够用不同的零件更换零件,并且汽车将继续工作。假设您的旧收音机没有数字调谐器,但您想收听高清收音机,因此您购买了带有高清接收器的新收音机。您应该可以将旧收音机取出并插入新收音机,只要它具有相同的接口即可。从表面上看,这意味着将收音机连接到汽车的电源插头在新收音机上的形状必须与旧收音机上的形状相同。如果汽车的插头是矩形的并且有 15 个针脚,那么新收音机的插孔也需要是矩形的并且也有 15 个针脚。

但除了机械配合外,还有其他考虑因素:插头上的电气性能也必须相同。如果旧无线电连接器上的针 1 为 +12V,则新无线电连接器上的针 1 也必须为 +12V。如果新收音机上的针脚 1 是“左扬声器输出”针脚,则收音机可能会短路或熔断。这将明显违反 LSP。

您还可以考虑降级的情况:假设您的昂贵收音机坏了,而您只能买得起 AM 收音机。它没有立体声输出,但它具有与现有收音机相同的连接器。假设规格中的针脚 3 是左扬声器输出,针脚 4 是右扬声器输出。如果您的 AM 收音机从针脚 3 和针脚 4 播放单声道信号,您可以说它的行为是一致的,这是可以接受的替代。但是,如果您的新 AM 收音机仅在针脚 3 上播放音频,而针脚 4 上不播放音频,则声音将不平衡,这可能不是可接受的替代品。这种情况也会违反 LSP,因为虽然您可以听到声音,并且没有保险丝熔断,但收音机不符合接口的完整规范。

于 2014-01-03T04:35:58.597 回答
2

首先,您需要定义什么是车辆和汽车。根据谷歌(不是很完整的定义):

车辆:
用于运送人或货物的东西,特别是。在陆地上,例如汽车、卡车或手推车。

汽车:
一种公路车辆,通常有四个轮子,由内燃机或
电动机提供动力,能够搭载少数人

所以汽车是交通工具,但交通工具不是汽车。

于 2013-12-31T17:51:55.057 回答
-4

在我看来,为了归档 LSP,子类型永远不能添加新的公共方法。只是私有方法和字段。当然,子类型可以覆盖基类的方法。如果一个子类型有一个基本类型没有的公共方法,那么您根本不能用基本类型替换子类型。如果您将实例传递给客户端的方法,从而您收到子类型的实例但参数的类型是基类型,或者如果您有一个基类型类型的集合,其中子类型也是其中的一部分,那么您如何调用子类型类的方法不使用 if 语句询问它的类型,并且如果类型匹配,则对该子类型进行强制转换以调用其上的方法。

于 2015-02-10T15:07:51.390 回答