【问题标题】:How can I avoid breaking Liskov Substitution Principle (LSP)?如何避免违反 Liskov 替换原则 (LSP)?
【发布时间】:2012-05-28 00:59:18
【问题描述】:

我的情况与 Code Complete 中提到的 Steve McConnell's 非常相似。只有我的问题是基于 Vehicles 和 Trike 恰好是在法律上属于 Cars 类别。到目前为止,汽车有四个轮子。无论如何,我的域都是不必要的复杂,所以很容易坚持下面的猫示例。

对覆盖例程且内部不执行任何操作的类持怀疑态度 派生的例程 这通常表明设计中的错误 基类。例如,假设你有一个类 Cat 和一个 例程 Scratch() 并假设你最终发现一些 猫是去爪的,不能抓挠。您可能会想创建一个 从名为 ScratchlessCat 的 Cat 派生的类并覆盖 Scratch() 例行公事什么都不做。这种方法存在几个问题:

它违反了 Cat 中提出的抽象(接口契约) 类通过改变其接口的语义。

当您将这种方法扩展到其他方法时,它很快就会失控 派生类。当你发现一只没有尾巴的猫时会发生什么?或者一个 不会抓老鼠的猫?还是一只不喝牛奶的猫? 最终你会得到像这样的派生类 ScratchlessTaillessMicelessMilklessCat。

随着时间的推移,这种方法会产生令人困惑的代码 维护,因为祖先类的接口和行为 暗示他们后代的行为很少或根本没有。

解决这个问题的地方不在基类,而是在 原始的猫类。创建一个 Claws 类并将其包含在 猫科。根本问题是假设所有的猫都会抓挠, 所以要从源头上解决这个问题,而不是仅仅把它包扎起来 目的地。

根据上面他伟大的书中的文字。跟随不好

父类不必是抽象的

public abstract class Cat {
   public void scratch() {
      System.out.println("I can scratch");
   }
}

派生类

public class ScratchlessCat extends Cat {
   @Override
   public void scratch() {
      // do nothing
   }
}

现在他建议创建另一个类Claws,但我不明白如何使用这个类来避免需要ScratchlessCat#Scratch

【问题讨论】:

  • Cat 示例很好地说明了 LSP。在了解 LSP 的过程中,我惊讶于 SO 和其他网站上有多少糟糕的示例以及存在多少误解。

标签: java oop liskov-substitution-principle


【解决方案1】:

并非所有的猫都有爪子并且能够抓挠,这是Cat 不应在其 API 中定义公共 scratch 方法的重要线索。第一步是考虑为什么首先定义scratch。如果可以的话,猫可能会在受到攻击时抓挠;如果不是,他们会发出嘶嘶声或逃跑。

public class Cat extends Animal {
    private Claws claws;

    public void onAttacked(Animal attacker) {
        if (claws != null) {
            claws.scratch(attacker);
        }
        else {
            // Assumes all cats can hiss.
            // This may also be untrue and you need Voicebox.
            // Rinse and repeat.
            hiss();
        }
    }
}

现在您可以将任何Cat 子类替换为另一个子类,它的行为是否正确取决于它是否有爪子。您可以定义一个DefenseMechanism 类来组织各种防御,例如ScratchHissBite 等。

【讨论】:

    【解决方案2】:

    您仍然有一个scratch() 方法,但它不会被派生类覆盖:

    public class Cat {
      Claw claw_;
      public Cat(Claw claw) {claw = claw_;}
      public final void scratch() {
        if (claw_ != null) {
          claw_.scratch(this);
        }
      }
    }
    

    这允许您将划痕逻辑委托给包含的Claw 对象(如果存在)(如果没有爪子,则不划痕)。从 cat 派生的类在如何划伤方面没有发言权,因此无需根据能力创建影子层次结构。

    另外,由于派生类不能改变方法实现,所以不存在破坏基类接口中scratch()方法的预期语义的问题。

    如果你把它推到极端,你可能会发现你有很多类,但没有很多派生类——大部分逻辑都委托给组合对象,而不是委托给派生类。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-08-16
      • 2015-01-01
      • 2011-09-09
      • 2012-01-12
      • 1970-01-01
      • 1970-01-01
      • 2015-09-18
      相关资源
      最近更新 更多