任何人都可以使用车辆提供Liskov替代原则(LSP)的示例吗?

问题描述 投票:31回答:5

Liskov替换原则规定子类型应该可替代该类型(不改变程序的正确性)。

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

我已经读过关于方形/矩形的例子,但我认为车辆的一个例子可以让我更好地理解这个概念。

oop solid-principles liskov-substitution-principle
5个回答
51
投票

对我来说,这个来自Uncle Bob(1996 Quote)的Robert C Martin最好地总结了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);
 }

Glaring Violation - 运行时类型切换

这是一个违反LSP的例子,使用RTTI,然后是Downcasting - 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 principle,因为为了容纳新的子类,将需要对函数进行连续修改。

违规 - 先决条件由子类型加强

另一个违规的例子是"pre condition is strengthened by a subtype"

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基类miles < 300方法的前提条件,现在最大不超过50英里。这是无效的,因为Vehicle的合同定义允许300英里。

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

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

微妙的违规 - 子类滥用接口实现

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

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"),例如,

  • 在一个新的界面IVehicleWithDoors,它可以继承IVehicle
  • 或更好的IMO,在一个单独的界面IDoor,然后像CarTruck这样的车辆将实现IVehicleIDoor接口,但ScooterMotorcycle不会。
  • 甚至3个接口,IVehicleDrive()),IDoorOpen())和IVehicleWithDoors,它们都继承了这两种。

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


19
投票

想搬家时,我想租一辆车。我打电话给租用公司,问他们有什么型号。他们告诉我,虽然我将获得下一辆可用的汽车:

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."); 
    }
}

我不高兴,因为我基本上被他们的小册子骗了 - 如果一级方程式赛车有一个看起来像是可以装行李而不会打开的假靴子也没关系,这对搬家来说毫无用处!

如果我被告知“这些都是我们所有汽车所做的事情”,那么我给的任何汽车都应该以这种方式行事。如果我不能相信他们的宣传册中的细节,那就没用了。这就是Liskov替代原则的本质。


3
投票

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

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

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

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


2
投票

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

车辆: 用于运送人员或货物的东西,尤指在陆地上,如汽车,卡车或推车。

汽车: 公路车辆,通常具有四个车轮,由内燃机或电动机提供动力 电机能够携带少数人

所以汽车是一种车辆,但车辆不是汽车。


-2
投票

在我看来,为了实现LSP,子类型永远不会添加新的公共方法。只是私人方法和领域。当然,子类型可以覆盖基类的方法。如果子类型具有basetype没有的单个公共方法,则根本无法使用basetype替换子类型。如果你将一个实例传递给客户端的方法,你接收一个子类型的实例,但参数的类型是basetype,或者如果你有一个类型basetype的集合,其中也有subytpes,那你怎么能调用子类的方法没有使用if语句询问它的类型,如果类型匹配,则对该子类型进行强制转换以便在其上调用方法。

© www.soinside.com 2019 - 2024. All rights reserved.