如何避免破坏里氏替换原理(LSP)?

2022-09-03 13:45:21

我的情况与史蒂夫·麦康奈尔(Steve McConnell)在《代码完成》(Code Complete)中提到的情况非常相似。只是我的问题是基于车辆和三轮车恰好是法律属于汽车类别。到目前为止,汽车有四个轮子。无论如何,我的领域是不必要的复杂,所以很容易坚持下面的猫的例子。

对重写例程且在派生例程中不执行任何操作的类持怀疑态度 这通常表示基类设计中存在错误。例如,假设你有一个类Cat和一个例行的Scratch(),并假设你最终发现一些猫是被禁止的,不能抓挠。您可能想创建一个名为 ScratchlessCat 的 Cat 派生类,并重写 Scratch() 例程以不执行任何操作。这种方法存在几个问题:

它通过更改其接口的语义违反了 Cat 类中呈现的抽象(接口协定)。

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

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

解决此问题的位置不在基类中,而是在原始 Cat 类中。创建一个 Claws 类,并将其包含在 Cats 类中。根本问题是假设所有的猫都抓挠,所以在源头上解决这个问题,而不仅仅是在目的地包扎它。

根据他上面的伟大书中的文字。以下不好

父类不必是抽象的

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

派生类

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

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


答案 1

并非所有的猫都有爪子并且能够抓挠,这是一个很大的线索,不应该在其API中定义公共方法。第一步是考虑您首先定义的原因。也许猫在受到攻击时可以抓挠;如果不是,他们会发出嘶嘶声或逃跑。Catscratchscratch

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();
        }
    }
}

现在,您可以将任何子类替换为另一个子类,并且它将根据它是否具有爪子而正常运行。您可以定义一个类来组织各种防御,例如 、 、 等。CatDefenseMechanismScratchHissBite


答案 2

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

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

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

此外,由于派生类无法更改方法实现,因此不存在它们在基类接口中破坏方法的预期语义的问题。scratch()

如果你把它推向极端,你可能会发现你有很多类,而不是很多派生 - 大多数逻辑被委托给组合对象,而不是委托给派生类。