英文:
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); --> 编译错误
}
}
我不确定这是否违反了它。该原则表示,如果您有一个属于类S的对象,那么您可以用类T的另一个对象来替换它,其中S是T的子类。然而,如果我写了以下代码:
Vehicle vehicle = new Vehicle();
function(vehicle);
当然会产生编译错误,因为Vehicle类没有openDoor()方法。但这意味着我无法用它们的父类Vehicle替换VehicleWithDoors对象,这似乎违反了该原则。所以这段代码是否违反了原则?
我需要一个很好的解释,因为我似乎无法理解它。
英文:
I'm trying to understand the Liskov substitution principle, and I have the following code:
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
}
}
I'm not sure if this violates it or not. The principle says that if you have an object of class S, then you can substitute it with another object of class T, where S is a subclass of T. However, what if I wrote
Vehicle vehicle = new Vehicle();
function(vehicle);
This of course gives compile error, because the Vehicle class doesn't have an openDoor() method. But this means I can't substitute VehicleWithDoors objects with their parent class, Vehicle, which seems to violate the principle. So does this code violate it or not?
I need a good explanation because I can't seem to understand it.
答案1
得分: 5
你把这个弄反了。该原则规定:如果 S
是 T
的子类型,则程序中类型为 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)可用于避免此违规情况,您可以声明 ReadableList
和 WritableList
接口。
另一种表示可能不支持添加项的方法是添加一个 canAddItem(item): boolean
方法。设计可能不够优雅,但它清楚地表明并非所有实现都支持该操作。
实际上,我更喜欢 LSP 的这个定义:LSP 表示每个子类必须遵守与超类相同的约定。"约定" 不仅可以在代码中定义(在我看来最好是这样),还可以通过文档等方式定义。
英文:
You got that backwards. The principle states that "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".
Basically, VehicleWithDoors
should work where Vehicle
works. That obviously doesn't mean Vehicule
should work where VehiculeWithDoors
work. Yet in other words, you should be able to substitute a generalization by a specialization without affecting the program's correctness.
A sample violation would be an ImmutableList
extending a List
that defines an add
operation, where the immutable implementation throws an exception.
<!-- begin snippet: js hide: false console: true babel: false -->
<!-- language: lang-js -->
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.");
}
}
<!-- end snippet -->
The Interface Segregation Principle (ISP) can be used to avoid the violation here, where you'd declare ReadableList
and WritableList
interfaces.
Another way to communicate that adding an item may not be supported could be to add a canAddItem(item): boolean
method. The design may not be as elegant, but it makes it clear not all implementation supports the operation.
I actually prefer this definition of the LSP: "LSP says that every subclass must obey the same contracts as the superclass".
The "contract" may be defined not only in code (better when it does IMO), but also through documentation, etc.
答案2
得分: 1
当您扩展一个类或接口时,新类仍然是它所扩展的类型。我认为最容易理解的方式是将子类视为超类的一种特殊类型。因此,它仍然是超类的实例,具有一些附加行为。
例如,您的 VehicleWithDoor
仍然是一个 Vehicle
,但它也有门。Scooter
也是一种车辆,但它没有门。如果您有一个方法来打开车门,车辆必须有门(因此,当您将踏板车传递给它时会出现编译时错误)。对于接受特定类的对象的方法也是如此,您可以传递其子类的实例对象,该方法仍然可以工作。
在实现方面,您可以安全地将任何对象转换为其超类型之一(例如,将 Car
和 Scooter
转换为 Vehicle
,将 Car
转换为 VehicleWithDoors
),但反过来不行(如果进行一些检查并进行显式转换,则可以安全地这样做)。
英文:
When you extend a class or an interface, the new class is still of the type that it extended. The easiest way to reason about this (IMO) is thinking of a subclass as a specialised type of the superclass. So it's still an instance of the superclass, with some additional behaviors.
For example, your VehicleWithDoor
is still a Vehicle
, but it also have doors. A Scooter
is a vehicle too, but it doesn't have doors. If you have a method to open a vehicle's door, the vehicle must have doors (hence the compile time error when you pass a scooter to it). The same is for a method that takes an object of a certain class, you can pass an object that is instance of its subclass and the method will still work.
In terms of implementation, you can safely cast any object to one of its supertype (e.g. Car and Scooter
to Vehicle
, Car
to VehicleWithDoors
), but not the other way around (you can safely do so if you do some checks and cast it explicitly).
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论