46

Liskov Substitution 原则SOLID的原则之一。我已经多次阅读这个原则并试图理解它。

这是我从中得到的,

该原则与类层次结构之间的强行为契约有关。子类型应该能够在不违反合同的情况下被超类型替换。

我也读过其他一些文章,我对这个问题有点迷茫。方法不Collections.unmodifiableXXX()违反 LSP 吗?

摘自上面链接的文章:

换句话说,当通过它的基类接口使用一个对象时,用户只知道基类的前置条件和后置条件。因此,派生对象不能期望这些用户遵守比基类要求的更强大的先决条件

为什么我这么认为?

class SomeClass{
      public List<Integer> list(){
           return new ArrayList<Integer>(); //this is dumb but works
      }
}

class SomeClass{
     public List<Integer> list(){
           return Collections.unmodifiableList(new ArrayList<Integer>()); //change in implementation
     }
}

我不能改变实现SomeClass以在将来返回不可修改的列表。编译将起作用,但是如果客户端以某种方式尝试更改List返回的内容,那么它将在运行时失败。

这就是 Guava为集合创建单独的ImmutableXXX接口的原因吗?

这不是直接违反 LSP 还是我完全搞错了?

4

4 回答 4

42

LSP 说每个子类都必须遵守与超类相同的契约。因此,是否是这种情况Collections.unmodifiableXXX()取决于该合同的阅读方式。

Collections.unmodifiableXXX()如果尝试对其调用任何修改方法,则返回的对象将引发异常。例如,如果add()被调用,UnsupportedOperationException则会抛出 an。

什么是总合同add()?根据API 文档,它是:

确保此集合包含指定的元素(可选操作)。如果此集合因调用而更改,则返回 true。(如果此集合不允许重复且已包含指定元素,则返回 false。)

如果这是完整的合同,那么确实不能在所有可以使用集合的地方使用不可修改的变体。但是,该规范仍在继续,并且还说:

如果一个集合拒绝添加一个特定元素,除了它已经包含该元素之外,它必须抛出一个异常(而不是返回 false)。这保留了在此调用返回后集合始终包含指定元素的不变量。

这显式地允许实现具有不将参数添加add到集合但导致异常的代码。当然,这包括收集客户的义务,即他们考虑到这种(法律)可能性。

因此,行为子类型(或 LSP)仍然得到满足。但这表明,如果计划在子类中具有不同的行为,则也必须在顶级类的规范中预见到。

顺便说一句,好问题。

于 2014-02-26T19:20:01.767 回答
13

是的,我相信你说的没错。本质上,要实现 LSP,您必须能够对子类型执行任何您可以对超类型执行的操作。这也是 LSP 出现椭圆/圆问题的原因。如果 Ellipse 有setEccentricity方法,而 Circle 是 Ellipse 的子类,并且对象应该是可变的,那么 Circle 就无法实现该setEccentricity方法。因此,你可以用椭圆做一些你不能用圆形做的事情,所以违反了 LSP。† 同样,你可以用一个常规做一些你List不能用 包裹的事情Collections.unmodifiableList,所以这是违反 LSP 的。

问题是这里有一些我们想要的东西(一个不可变的、不可修改的、只读的列表),它没有被类型系统捕获。在 C# 中,您可以使用IEnumerablewhich 捕获序列的概念,您可以迭代和读取,但不能写入。但是在 Java 中只有List,它通常用于可变列表,但有时我们希望将其用于不可变列表。

现在,有些人可能会说 Circle 可以实现setEccentricity并简单地抛出异常,类似地,不可修改的列表(或 Guava 中的不可变列表)在您尝试修改它时会抛出异常。但这并不意味着从 LSP 的角度来看它是一个List。首先,它至少违反了最小惊喜原则。如果调用者在尝试将项目添加到列表时遇到意外异常,那是相当令人惊讶的。如果调用代码需要采取措施来区分一个它可以修改的列表和一个它不能修改的列表(或者一个它可以设置的偏心率的形状,而一个它不能设置的形状),那么一个不能真正替代另一个.

如果 Java 类型系统有一个只允许迭代的序列或集合类型,以及另一个允许修改的类型,那就更好了。也许 Iterable 可以用于此,但我怀疑它缺少一些size()人们真正想要的功能(如 )。不幸的是,我认为这是当前 Java 集合 API 的限制。

一些人已经注意到,文档Collection允许实现从方法中抛出异常add。我想这确实意味着一个无法修改的列表在涉及合同时遵守法律条文,add但我认为应该检查一个人的代码,看看有多少地方可以保护对 mutating 方法的调用在争论 LSP 没有被违反之前,用 try/catch 块列出 ( add, addAll, remove, )。clear也许不是,但这意味着所有调用List.add它作为参数接收的 List 的代码都被破坏了。

那肯定会说很多。

(类似的论点可以表明,作为null每个类型的成员的想法也违反了 Liskov 替换原则。)

† 我知道还有其他方法可以解决椭圆/圆问题,例如使它们不可变,或删除 setEccentricity 方法。作为类比,我在这里只谈论最常见的情况。

于 2014-02-26T19:27:23.053 回答
5

我不认为这是违规行为,因为合同(即List接口)说变异操作是可选的。

于 2014-02-26T19:11:25.547 回答
-1

我认为你没有在这里混合东西。
LSP

Liskov 的行为子类型概念定义了可变对象的可替代性概念;也就是说,如果 S 是 T 的子类型,则程序中类型 T 的对象可以用类型 S 的对象替换,而不会改变该程序的任何所需属性(例如,正确性)。

LSP 指的是子类

List 是一个接口而不是超类。它指定一个类提供的方法列表。但是这种关系不像父类那样耦合。A 类和 B 类实现相同接口的事实并不能保证这些类的行为。一个实现可以始终返回 true,而另一个实现抛出异常或始终返回 false 或其他任何实现,但在实现接口的方法时都遵循接口,因此调用者可以调用对象上的方法。

于 2014-02-26T23:22:03.537 回答