IS-A 和 Liskov 替换原则之间的区别?

Difference between the IS-A and Liskov Substitution Principle?

我只是想知道 IS-A(即 UML 术语和 OOP)和 Liskov 替换原则 (LSP) 之间是否存在差异?

其实都是在说继承。那么实践中的主要区别是什么?

这两个术语最终描述的是相同的 "concept"。

Liskov 替换原则 告诉你:classes B(基)和 C(子)之间的继承关系是合理的,当B 类型的某些对象...可以用 C 类型的对象替换。

这意味着:B 定义了一个 API 和一个 public 合同 - C 也必须维护这些属性!

IS-A 相当于同一件事:C 的某个对象也是 一个 B。

"difference" 是:LSP 为您提供精确的 规则,您可以检查。而 IS-A 更多的是 "observation" 或意图表达。比如:你表示你希望 class C IS-A B。

换句话说:当你不知道如何正确使用继承时,IS-A 不会帮助你写出正确的代码。而 LSP 清楚地告诉您类似:

class Base { int foo(); }
class Child extends Base { @Override double foo(); }

无效。根据 LSP,您只能 widen 方法参数和 restrict return 值。

int iValue = someBase.foo();

无法替换为

int iValue = someChild.foo();

因为 foo() 方法在其结果中 加宽了

最后一个想法:许多人认为 C IS-A B 与写下 Child extends Base 相同。是的。但这只是告诉编译器 C 扩展了 B。这并不意味着你在 C 中使用的方法将遵循 LSP,从而将 C 变成 B 的 real 有效后代。

C IS-A B 比 "C extends B" 需要 。要真正有效,必须坚持LSP!

Is-A/Has-A是关于是否使用继承。 laserCat 是一种激光,还是它应该只有一个激光场?如果您以某种方式使用继承,LSP 是一个需要注意的特定问题。

继承的好处是拥有Animal a1;指向猫或狗,使用 a1.speed() (*)。 LSP 表示 Cat 和 Dog 速度函数需要使用相同的单位。同样,Cats 的 a1.setWeight 不允许负权重,但 Dogs 将它们更改为 0。当您可以调用任一函数时,LSP 是关于一致性的。如果您已经知道 Animal a1,这实际上非常明显;技巧,很难。

作为对比,假设您有独立的猫狗游戏。如果实际上速度的测量方式不同,那么让猫使用公制而狗使用英语就可以了。如果 Cats and Dogs 继承自 Animal,但您从不使用 "a1 = Cat or Dog" 技巧,它仍然可以。 c1.speed() 肯定是公制,d1.speed() 显然是每小时英里数。但是如果你有函数 animalRace(Animal a1),你就有问题了。

不同之处还在于语气。 Is-a/has-a 是对初学者的简单建议。 LSP 来自一篇 30 年前为博士撰写的论文。它使用的方程式适用于研究生 Com Sci 专业。它使用 Pre 和 Post 条件,这些条件在当时是常见的、众所周知的术语。 "Substitution" 是一个很好的数学术语,但今天我们只说 "a base class that will be pointing to any subclass"。

(*) 更详细地说:我们有超级class 动物,还有子class 猫和狗。 Animal 有一个存根速度函数,Cat 和 Dog 各自覆盖它。 a1.speed() 查找正确的。一个真实的例子是一系列动物,其中真正包含猫和狗。或带有动物输入的函数,需要猫或狗。

(same *) 通常基础 class 是抽象的——我们永远不会创建 Animal 对象。但是如果我们有一个 Toaster superclass 和一个 DeluxeToaster subclass,技巧是一样的。任何带烤面包机的东西都可能带走任何 "is-a" 烤面包机。

继承是一种纯粹的句法关系,而里氏替换也是一种语义关系。

语法很简单:您学习如何将一个 class 声明为另一个的子代,然后编译器会告诉您是否编写了有效代码。如果代码编译通过,您就创建了继承关系 (IS-A)。

语义更难:它们决定了代码对客户意味着什么。语义通常包括诸如文档之类的东西。在流行的 OOP 语言中,编译器无法告诉您代码是否符合或违反其预期含义。这就是 Liskov 介入的地方。