【问题标题】:Liskov substitution principle and StreamsLiskov 替换原理和 Streams
【发布时间】:2013-12-15 22:47:41
【问题描述】:

存在无法编写或查找的 Stream 派生类这一事实是否违反了 Liskov 替换原则?

例如,NetworkStream 无法查找,如果调用了方法Seek,则会抛出NotSupportedException

还是因为CanSeek标志的存在就ok了?

考虑到众所周知的Square 继承自Rectangle 的示例... 将标志DoesHeightAffectsWidthDoesWidthAffectsHeight 添加到Rectangle 是否可以解决问题?

这不是为通过添加标志来修复问题打开了大门吗?

【问题讨论】:

  • 我认为关于IList<T> 接口可以问同样的问题——大多数时候它只是被视为一个常规的、可变的索引集合,但它有IsReadOnly 标志和Add 可以抛出(最简单的例子:IList<int> intList = new int[3];)......不过还是很有趣的问题。
  • 对。实际上我有这个问题是因为我正在阅读“数组与列表”的咆哮:)
  • @PatrykĆwiek:ICollection<T> 继承的ICollection<T>IsReadOnly 属性命名不佳,因为它指示对象是否可以添加或删除项目,而不是该项目是否“已读” -only”在任何更广泛的意义上。恕我直言,一个好的“列表”界面至少应该包括,(1)大小是不可变的吗? (2) 现有元素是不可变的吗? (3) 这个引用可以用来添加/删除项目吗? (4) 这个参考可以按索引写东西吗? (5) 这个引用是否可以安全地暴露给应该修改集合的代码? (6) 它能接受所有U类型的东西吗?

标签: c# oop stream solid-principles liskov-substitution-principle


【解决方案1】:

CanSeek 在技术上防止流类违反 LSP。只有当它返回 true 时,寻求承诺才会起作用。

我个人认为它是 ISP 和可能的 SRP 的严重弯曲,我的内部设计者会更喜欢类似 SeekableStream 子类/接口的东西,可搜索的流可以从中继承。但我确信这会带来其自身的问题(例如,在只有有时可搜索的流中)......坦率地说,现实世界的可用性胜过原则。

这是需要牢记的。有时,原则和现实会发生冲突。在大多数情况下,SOLID 原则有助于最大限度地减少不必要的复杂性,并且通常保持 OO 系统可维护并防止它们因自身重量而崩溃。但是,如果纯度导致系统更加复杂——例如,因为现在只有有时可搜索的流不能很好地融入层次结构——那么也许偶尔会有一点丑陋是有道理。

但它绝不应该仅仅因为法律条文允许就成为首选。 SOLID 原则不仅仅是规则;它们是原则。它们是文字背后的理念——法律的精神。如果你在用律师的方式超越精神的同时坚持信条,那么你就错过了原则的全部要点。

至于方形/矩形问题...从技术上讲,具有确定改变高度是否也会改变宽度的属性/函数,可以考虑与 LSP 的 字母 保持一致。不过,再一次,它感觉就像是在做律师,并且正在推动其他 SOLID 原则的界限。从现实的角度来看,这也绝对不是最佳解决方案,因为它增加了复杂性并引入了意外副作用的可能性;现在所有想说的rect.Height = 50; 都可以无意中改变宽度。

【讨论】:

  • 我不认为“canXX”方法违反了 ISP。在我看来,重要的是接口实现可以提供它们拥有的所有功能,而无需承诺任何它们不提供的功能。如果所有这些区别都必须在类型系统中表达(以避免编写大量冗余的canXX 方法,接口可以提供单个 features 方法,该方法将返回描述实施者的可能单例对象)。
  • @supercat:是的,如果在类型系统中没有干净的方法可以做到这一点,你可能不会正式break ISP 这样做一两次。但是,如果您现在必须在进行基本操作之前说 if (!rect.DoesWidthAffectHeight)if (stream.CanSeek),那您就是在屈服于它。
  • @cHao:约束矩形的例子有点牵强,但是像流和集合这样的东西可能有很多不同的能力组合,试图为每个组合设置不同的类型会是行不通。
  • @supercat:或者,您可以使用支持多重继承的语言。 :P 或Seekable 接口。或者,如果您需要寻找,只需尝试这样做,让它在不支持它的类型上失败。
  • @cHao:问题不只是继承。如果要设计一个集合包装器,其构造函数接受事件接收者和集合接口,并公开与底层集合相同的能力,而且每次更改时都发送一个事件,那么一种类型将能够处理所有集合而无需考虑他们是否支持addsetItem,或者两者都支持。如果不使用通用接口,则需要一个包装器用于支持add 但不支持setItem 的东西,一个用于支持setItem 但不支持add 的东西,以及一个用于.. .
【解决方案2】:

Can... 方法意味着Stream 不会破坏 LSP。 Stream 提供能力 来读取、写入和查找,但不保证任何实现类都会尊重它。 Can... 方法使它成为Stream 契约的一个显式特性——派生类必须实现它们以允许客户端代码在调用之前检查派生类是否实现了某些行为。因此,任何尝试写入Stream 的代码都应该在调用Write 之前检查CanWrite,这可以通过Stream 的任何正确实现的派生来完成。因此,它们可以根据 LSP 的要求互换。

我认为添加方法来标记派生类是否实现特定功能可能会被滥用,这当然是正确的——如果团队没有纪律,他们最终可能会得到一个非常宽泛、臃肿的接口,从而破坏 ISP。我认为StreamIList<T> 在这方面设计得很好——它们不会破坏LSP,并且它们定义了一个足够窄的密切相关行为的契约以留在ISP 内。显然,他们的设计已经考虑过了。

我认为在 Square 继承自 Rectangle 的情况下,您当然可以添加 DoesHeightAffectsWidthDoesWidthAffectsHeight 来解决问题,但团队必须决定这是否可以接受,或者是否添加这些方法破坏了 ISP。添加AreAllInternalAnglesEqual 来支持梯形是不是太过分了?在某种程度上,这取决于编写代码的工程师。

【讨论】:

    猜你喜欢
    • 2017-10-18
    • 2013-10-15
    • 1970-01-01
    • 1970-01-01
    • 2013-08-23
    • 2019-06-26
    • 1970-01-01
    • 2014-06-05
    • 2010-12-03
    相关资源
    最近更新 更多