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

ran*_*512 33 oop liskov-substitution-principle solid-principles

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

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

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

Stu*_*tLC 54

对我来说,1996年来自鲍勃叔叔(Robert C Martin)的报价总结了最好的LSP:

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

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

  • 任何使用类或接口抽象的代码都必须假定除了定义的抽象之外的其他类;
  • 超类的任何子类或抽象的实现必须遵守抽象接口的要求和约定.

LSP合规性

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

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

IVehicle停留在LSP范围内的消费者的这种实现:

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

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.
 }
Run Code Online (Sandbox Code Playgroud)

违规方法超出了签约IVehicle接口,并且破解了接口的已知实现(或子类,如果使用继承而不是接口)的特定路径.Bob叔叔还解释说,使用类型切换行为的LSP违规通常也违反了Open和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);
     }
 }
Run Code Online (Sandbox Code Playgroud)

在这里,Scooter子类试图违反LSP,因为它试图加强(进一步约束)基类Drive方法的前提条件miles < 300,到现在最多不到50英里.这是无效的,因为合同定义Vehicle允许300英里.

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

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

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

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;}}
}
Run Code Online (Sandbox Code Playgroud)

在这里,无论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);
}
Run Code Online (Sandbox Code Playgroud)

编辑,回复:OpenDoor

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

  • 在一个IVehicleWithDoors可以继承的新界面上IVehicle
  • 或IMO更好的是,在一个单独的接口IDoor,然后样车Car,并Truck会同时实现IVehicleIDoor接口,但ScooterMotorcycle不会.
  • 甚至是3个接口,IVehicle(Drive()),IDoor(Open()),IVehicleWithDoors它们都继承了这两个接口.

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


ano*_*ave 22

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

public class CarHireService {
    public Car hireCar() {
        return availableCarPool.getNextCar();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

public interface Car {
    public void drive();
    public void playRadio();
    public void addLuggage();
}
Run Code Online (Sandbox Code Playgroud)

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

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."); 
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

  • 这实际上是接口隔离原理的一个例子,不是吗? (8认同)
  • 接口隔离原则可以从另一个角度解决问题(因为接口不会做出具体类无法保留的承诺) (2认同)