【问题标题】:Liskov substitution design principle adaptationLiskov替换设计原理适配
【发布时间】:2017-01-05 21:18:30
【问题描述】:

假设我有一个抽象类bird,它的一个功能是fly(int height)。

我有许多不同的鸟类类,每个类都有自己不同的飞行实现,并且该函数在整个应用程序中广泛使用。

有一天我的老板来要求我添加一只鸭子,它可以做其他鸟做的所有事情,只是它不会飞,而是在应用程序的池塘里游泳。

将鸭子添加为鸟的子类型违反了 Liskov 替换规则,因为在调用duck.fly 时,我们要么抛出异常,不执行任何操作,要么违反正确性原则。

您将如何在牢记 SOLID 设计原则的同时引入此更改?

【问题讨论】:

  • 并非真正违反 LSD。退化的实现只是破坏 LSD 的一个迹象。您应该考虑您的 fly() 方法的合同。如果 Duck 的 fly() 什么都不做,客户端会受到什么影响?
  • 这个问题和著名的三角形和正方形问题类似。我认为鸭子不应该是鸟类的子类型。
  • 不违反 LSP,除非您在 duck.fly() 中抛出异常,或者您违反了该方法的某些后置条件或 Bird 类的不变量(在这样的人为的例子)
  • 这是一种违规行为,提醒您 - 规则说,如果您的类 S 是 T 的子类型,那么您应该能够将 T 换成 S 并且仍然会得到类似的行为。什么都不做或抛出异常总是被定义为违反规则。如果您使用 java 附带的某些功能却发现它什么也没做,您会怎么想? :)

标签: design-patterns design-principles liskov-substitution-principle


【解决方案1】:

您发现的问题是由于您的模型的以下不兼容功能造成的。

  1. 所有的鸟都会飞
  2. 鸭子是鸟
  3. 鸭子不会飞

您需要更改其中一项以使语句保持一致。更改哪个既是最适合您使用的设计的问题,也是进行更改所涉及的问题。

您可以通过引入类来区分会飞和不会飞的鸟类来解决这个问题。也许为了让变化更小,你的鸟类继续飞,所以鸭子不是鸟,而是不会飞的鸟。或者,也许您扩展鸟类以创建 flybirds 类并将 fly 方法移到那里 - 两者都将涉及对现有代码的更改。

在第 3 点上抛出异常有点作弊——你的鸭子仍然不会飞,它只是使问题成为运行时问题而不是设计时问题。这可能很快,但它不是一个非常安全的方法,它需要调用代码来避免对碰巧是鸭子的鸟调用 fly。

是否允许 fly 对鸭子不做任何事情取决于您的调用代码通常期望 fly 做什么 - 实际上您通过将第 1 点中的 fly 的含义更改为“可以要求所有鸟类飞(虽然有些不会)”。在这种情况下,fly 真的变成了 flyIfyouCan,这可能表明设计不完善,但可能是实用的解决方案——尤其是在调整现有设计时。

您建议什么都不做选项的事实可能表明一条动荡最小的路线,因为如果您确实让苍蝇对鸭子什么都不做,那么您仍然需要一些特定于鸭子的代码来让它游泳,所以您可以接受在现实世界中鸭子可以飞,并且鸭子特定的代码调用游泳而不是飞 - 不如飞。

更一般地说,我认为您实际上描述的是第 1 点从“所有鸟儿都可以飞”到“所有鸟儿都可以移动”的变化,然后非鸭子实现移动如飞,鸭子实现移动如游泳(无论是他们也有飞行方法或没有)。这可能会涉及将一些现有的呼叫改为 fly 的呼叫。

【讨论】:

  • 我喜欢用flyingBirds 和flightlessBirds 子输入鸟类抽象类的想法。我没有建议什么都不做/抛出异常,我说这会违反替换规则。
  • 这意味着我们同意那个例外。什么都不做的方法并不总是坏的 - 在这种情况下,什么都不做会很奇怪(我期待一个涉及鸟类位置变化的后置条件),但在某些实际情况下,如果什么都不做也可以符合预期的后期条件。
  • 一般来说,我会尽量让模型尽可能地接近真实世界的情况。这使得它对需求更改更具前瞻性(当然,有时我们会收到不现实的更改请求,但我们无法轻易预测这些更改请求会是什么)
【解决方案2】:

我看到了 3 个选项:

  1. 使用Bird作为通用功能的抽象基类,并从它派生FlyingBirdAquaticBird

  2. 使用 Zoran Horvat 所描述的对象组合和访问者模式:https://vimeo.com/195774910(无论如何都值得一看)——尽管在手头的情况下这似乎有点矫枉过正。

    李>
  3. 使用您描述的解决方案。

最后,一切都是为了平衡。 如果您期待许多具有不同能力的不同鸟类加入,那么您应该认真考虑选项 2,否则取决于您以后如何使用课程,在 1 和 3 之间进行选择。

根据 Gilads cmets 更新选项 2(对象组合和访问者模式)

这里的主要内容是你可以拥有一个类Bird 并将能力附加到它上面,例如FlyingSwimming,也许稍后你会决定你有其他能力或某种分类。您可以通过创建不同的接口(@98​​7654329@、ISwimmable)来实现,并具有以下内容:

public class SomeFlyingBird : BirdBase, IFlyable 
{
 ....
}

public class Duck : BirdBase, ISwimmable
{
 ....
}

然后使用访问者模式来访问具体的动作。

或者,如果您想要更健壮和优雅的东西,您可以为每个能力创建不同的类并将它们附加到 Bird 类:

public class Bird/Animal
{
     string name;
     List<Ability> abilities = new List<Ability>();

     public Bird (string name)
     {
         this.name = name;
     }

     public void Add (Ability ability)
     { 
          this.abilities.Add(ability);
     }
}

您可以稍后使用一些静态类来创建鸟类:

public static Bird CreateDuck ()
{
     var duck = new Bird("Donald");
     duck.Add(new SwimAbility());
     return duck;
}

技能和游泳可以是这样的:

public abstract class Ability
{
    public virtual void Accept(AbilityVisitor visitor) =>
        visitor.Visit(this);
}

public class SwimAbility : Ability
{
    public override void Accept(AbilityVisitor visitor)
    {
        base.Accept(visitor);
        visitor.Visit(this);
    }
}

我没有附上完整的代码,因为它不是我的,而且这个答案太长了。我再说一遍,对于你所描述的情况,这看起来有点矫枉过正。

我强烈建议您观看 Zoran Horvat 的视频并下载并使用他提供的代码: http://www.postsharp.net/blog/post/webinar-recording-object-composition

【讨论】:

  • 我无法播放视频,选项 2 听起来很有趣,您能详细说明一下吗?
  • 我调查了访问者,但我看不出它如何以任何优雅的方式解决手头的问题
  • 可以通过这种方式完成,但我同意您的 cmets 的观点,即在这种情况下这太过分了。另一点是它在运行时添加了能力,这使得确保所有鸭子的构造方式都赋予它们游泳能力变得更加困难,并且会使测试变得更加困难(只是因为你记得在你的单元测试代码不代表真实代码有)。
  • @ROX 虽然这是在运行时完成的,但具体类的创建应该由 一个 易于测试的静态类处理。当然,总是有可能误用代码,以后有人可以添加额外的能力。通过智能设计和工厂方法,也可以解决这个问题。
  • @OfirW,问题在于按预期使用测试类并不能确保没有没有通过静态类的情况。你可以做一些事情,比如将构造函数设为私有,并让静态类成为朋友来防止这种情况发生。我只是指出一个缺点,我并不是说这种设计没有用处。
【解决方案3】:

所以你的问题是

(1) 鸭子是鸟。

(2) 但它不会飞。

所以真正的问题是,

  • 您已经实现了您的 Bird 类假设“所有鸟类都会飞”
  • 但事实并非如此。因此,您要么事先没有考虑过极端情况(如 Duck),要么不打算拥有像 Duck 这样的类。

解决方案:

所以这里要采用的最简单和最明智的解决方案是使用通用名称作为方法名称,以便双方都能达成一致。所以在这里您可以将您的方法 Bird.fly() 重命名为 Bird.move()

所以 Duck.move() 和 Crow.move() 都有意义,但它们以自己的方式做。

所有其他可用的解决方案都会杀死多态性(使用两个不同的基类)。这意味着您将无法通过一种方法同时调用 Duck 和 Crow 来移动(分别游泳和飞行)。 (就像 Bird.move() 一样)。

在构建应用程序时会花费很多,因为您必须始终通过显式或隐式类型转换来区分它们,这非常糟糕。它会浪费你的时间并使你的应用程序很难扩展和维护。 :))

【讨论】:

  • 在这个问题中给出错误的假设是故意的,在这种情况下很明显,但在现实生活中的复杂系统中,很难预测我们可以和不可以对接口假设什么。关于移动建议 - 我不太喜欢它,因为它太通用了,我正在寻找更多动态行为方向的解决方案,可能使用组合或一些设计模式。
  • @GiladBaruchian 我同意你的假设。其实我只是指出了真实情况,并不是说应该避免。但我不认为你对解决方案有正确的意图。这是最好的动态解决方案。通用并不是拒绝的重点。你在问一个模式。问题是你应该严格避免将模式拖放到你的代码中,除非它真的反映了你的问题。除非您实际上最终会创建反模式并在很长一段时间内伤害自己。
猜你喜欢
  • 2013-12-15
  • 2017-10-18
  • 2013-08-23
  • 1970-01-01
  • 2013-10-15
  • 1970-01-01
  • 1970-01-01
  • 2010-12-03
  • 1970-01-01
相关资源
最近更新 更多