试图理解 Liskov 替换原则

Trying to understand Liskov substitution principle

我正在尝试了解Liskov 替换原则,我有以下代码:

class Vehicle {
}

class VehicleWithDoors extends Vehicle {
    public void openDoor () {
        System.out.println("Doors opened.");
    }
}

class Car extends VehicleWithDoors {
}

class Scooter extends Vehicle {
}

class Liskov {
    public static void function(VehicleWithDoors vehicle) {
        vehicle.openDoor();
    }

    public static void main(String[] args) {
        Car car = new Car();
        function(car);
        Scooter scooter = new Scooter();
        //function(scooter);  --> compile error
    }
}

我不确定这是否违反了它。该原则说,如果你有一个 class S 的对象,那么你可以用另一个 class T 的对象替换它,其中 S 是 T 的子 class。但是,如果我写了

Vehicle vehicle = new Vehicle();
function(vehicle);

这当然会产生编译错误,因为 Vehicle class 没有 openDoor() 方法。但这意味着我不能用它们的父对象 class,Vehicle 替换 VehicleWithDoors 对象,这似乎违反了原则。那么这段代码到底有没有违规呢? 我需要一个很好的解释,因为我似乎无法理解它。

你搞反了。该原则指出 "if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program".

基本上,VehicleWithDoors 应该在 Vehicle 工作的地方工作。这显然并不意味着 Vehicule 应该在 VehiculeWithDoors 工作的地方工作。然而换句话说,您应该能够在不影响程序正确性的情况下用专业化代替泛化。

一个示例违规是 ImmutableList 扩展 List,它定义了 add 操作,其中不可变的实现抛出异常。

class List {
  constructor() {
    this._items = [];
  }
  
  add(item) {
    this._items.push(item);
  }
  
  itemAt(index) {
    return this._items[index];
  }
}

class ImmutableList extends List {
  constructor() {
    super();
  }
  
  add(item) {
    throw new Error("Can't add items to an immutable list.");
  }
}

接口隔离原则 (ISP) 可用于避免此处的违规行为,您可以在此处声明 ReadableListWritableList 接口。

表达可能不支持添加项目的另一种方式是添加 canAddItem(item): boolean 方法。设计可能不那么优雅,但很明显并非所有实现都支持该操作。

我实际上更喜欢 LSP 的这个定义:"LSP says that every subclass must obey the same contracts as the superclass"。 “合同”不仅可以在代码中定义(在 IMO 中更好),还可以通过文档等方式定义。

当您扩展 class 或接口时,新的 class 仍然是它扩展的类型。对此进行推理的最简单方法(IMO)是将 subclass 视为 superclass 的特殊类型。所以它仍然是 superclass 的一个实例,具有一些额外的行为。

例如,您的 VehicleWithDoor 仍然是 Vehicle,但它也有门。 Scooter 也是一种交通工具,但它没有门。如果您有打开车门的方法,则该车必须有门(因此当您将滑板车传递给它时会出现编译时错误)。同样的,对于一个接受某个class的对象的方法,你可以传递一个作为其子class实例的对象,该方法仍然有效。

在实现方面,您可以安全地将任何对象转换为其超类型之一(例如 Car 和 Scooter 转换为 VehicleCar 转换为 VehicleWithDoors),但反之则不然(如果你做了一些检查并明确地转换它,你就可以安全地这样做)。