如何避免破坏Liskov替换原则(LSP)?

我的情况与Steve Complete所提到的Steve McConnell非常相似。 只有我的问题是基于车辆和三轮车才恰好依据法律属于汽车类别。 到目前为止,汽车有四个轮子。 任何方式我的域都不必要地复杂,所以很容易坚持下面的猫示例。

怀疑覆盖例程的类并在派生例程中不执行任何操作这通常表示基类设计中的错误。 例如,假设您有一个Cat类和一个例程Scratch(),并假设您最终发现某些猫被声明并且无法划伤。 您可能想要创建一个派生自Cat的名为ScratchlessCat的类,并重写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 } } 

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

你仍然会有一个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()方法的预期语义。

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

并非所有的猫都有爪子并且能够抓挠是一个很大的线索, 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等。