尝试理解里氏替换原则。

huangapple go评论59阅读模式
英文:

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

你把这个弄反了。该原则规定:如果 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 表示每个子类必须遵守与超类相同的约定。"约定" 不仅可以在代码中定义(在我看来最好是这样),还可以通过文档等方式定义。

英文:

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(&quot;Can&#39;t add items to an immutable list.&quot;);
  }
}

<!-- 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 也是一种车辆,但它没有门。如果您有一个方法来打开车门,车辆必须有门(因此,当您将踏板车传递给它时会出现编译时错误)。对于接受特定类的对象的方法也是如此,您可以传递其子类的实例对象,该方法仍然可以工作。

在实现方面,您可以安全地将任何对象转换为其超类型之一(例如,将 CarScooter 转换为 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).

huangapple
  • 本文由 发表于 2020年9月27日 23:38:51
  • 转载请务必保留本文链接:https://go.coder-hub.com/64090262.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定