尝试理解Liskov替换原则

4

我正在努力理解 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
    }
}

我不确定这是否违反了原则。该原则指出,如果您有一个类S的对象,则可以将其替换为类T的另一个对象,其中S是T的子类。然而,如果我写下以下代码:

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

当然,这会导致编译错误,因为Vehicle类没有openDoor()方法。但这意味着我无法用其父类Vehicle替代VehicleWithDoors对象,这似乎违反了该原则。那么,这段代码是否违反了该原则呢?我需要一个好的解释,因为我似乎无法理解它。

1
这意味着任何继承自VehicleWithDoors的子类都应该是该方法可接受的参数。 - Michael
3
我认为你的理解是相反的。如果 ST 的子类型,那么在代码中可以用 S 替换任何 T 的出现。但是不能T 替换 S 的出现。请注意不改变原意。 - Abra
我明白了,那我误解了。那么这个代码以这种形式是正确的吗? - raiyan
你应该将 openDoor() 方法放在 Vehicle 类中。 - NomadMaker
2个回答

5
你理解错了。这个原则的规定是 "如果 ST 的子类型,那么程序中类型为 T 的对象可以被类型为 S 的对象替换,而不会影响该程序的任何期望行为"
简单来说,VehicleWithDoors 应该能够在 Vehicle 可用的地方使用。当然这并不意味着 Vehicule 能在 VehiculeWithDoors 所在的位置工作。换句话说,你应该能够将概括用特化代替,而不会影响程序的正确性。
一个违反该原则的示例是将 ImmutableList 扩展为具有定义 add 操作的 List,而不可变实现则会抛出异常。

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表示每个子类必须遵守与超类相同的契约”。“契约”不仅可以在代码中定义(在我看来最好),还可以通过文档等方式定义。

2
不可变列表是违反的一个很好的例子。 - Totò

1
当您扩展一个类或接口时,新的类仍然是它所扩展的类型。我个人认为,理解子类是超类的一种特殊类型是最简单的方法。因此,它仍然是超类的实例,并具有一些额外的行为。
例如,您的`VehicleWithDoor`仍然是一个`Vehicle`,但它也有门。`Scooter`也是一种车辆,但它没有门。如果您有一个打开车门的方法,则该车必须有门(因此当您将滑板车传递给它时会出现编译时错误)。对于需要某个类的对象的方法,您可以传递其子类的实例,该方法仍将起作用。
在实现方面,您可以安全地将任何对象强制转换为其超类型(例如,将汽车和`scooter`转换为`Vehicle`,`Car`转换为`VehicleWithDoors`),但不能反过来(如果您进行一些检查并显式地进行转换,则可以安全地这样做)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接