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

时间:2013-12-31 17:33:14

标签: oop solid-principles liskov-substitution-principle

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

  • 有人可以在车辆(汽车)领域提供这个原则的例子吗?
  • 有人可以在车辆领域提供此原则违规行为的示例吗?

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

5 个答案:

答案 0 :(得分:51)

对我来说,来自 Bob叔叔1996 Quote)的这个Robert C Martin总结了最佳LSP:

  

使用指针或对基类的引用的函数必须能够使用派生类的对象而不知道它。

最近,作为基于(通常是抽象的)基类/超类的子类的继承抽象的替代,我们还经常使用 interfaces 进行多态抽象。 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,然后使用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,需要这些接口的对象的代码不应该向下转换接口以访问额外的功能。代码应该选择所需的适当的最小接口/(超级)类,并坚持该接口上的合同功能。

答案 1 :(得分:19)

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

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

但他们给了我一本宣传册,告诉我他们所有的模特都有这些功能:

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

这听起来正是我正在寻找的,所以我预订了一辆汽车&amp;走开快乐。在搬家那天,一辆一级方程式赛车出现在我家外面:

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替代原则的本质。

答案 2 :(得分:3)

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

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

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

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

答案 3 :(得分:2)

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

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

汽车:
一种公路车辆,通常带有四个车轮,由内燃机或电动车驱动,能够携带少量人员

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

答案 4 :(得分:-2)

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