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

Jav*_*aby 15 java oop liskov-substitution-principle

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

怀疑覆盖例程的类并在派生例程中不执行任何操作这通常表示基类设计中的错误.例如,假设您有一个类Cat和一个例程Scratch(),并假设您最终发现某些猫被声明并且无法划伤.您可能想要创建一个派生自名为ScratchlessCat的Cat的类,并覆盖Scratch()例程以不执行任何操作.这种方法存在几个问题:

它通过改变其接口的语义来违反Cat类中提供的抽象(接口契约).

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

随着时间的推移,这种方法会导致代码难以维护,因为祖先类的接口和行为对其后代的行为意味着很少或根本没有.

解决此问题的地方不在基类中,而是在原始Cat类中.创建一个Claws类并在Cats类中包含它.根本问题是假设所有猫都抓了,所以在源头修复这个问题,而不是仅仅在目的地包扎它.

根据他上面的伟大着作中的文字.以下是坏事

父类不必是抽象的

public abstract class Cat {
   public void scratch() {
      System.out.println("I can scratch");
   }
}
Run Code Online (Sandbox Code Playgroud)

派生类

public class ScratchlessCat extends Cat {
   @Override
   public void scratch() {
      // do nothing
   }
}
Run Code Online (Sandbox Code Playgroud)

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

Dav*_*ess 11

并非所有猫都有爪子并且能够抓挠是一个很大的线索,Cat不应该scratch在其API中定义公共方法.第一步是考虑您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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,您可以将任何子Cat类替换为另一个子类,它将根据是否具有爪子而正确运行.你可以定义一个DefenseMechanism类来组织各种防御如Scratch,Hiss,Bite,等


Att*_*ila 9

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

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

这允许您将抓取逻辑委派给包含的Claw对象(如果存在)(如果没有爪则不会刮擦).从cat派生的类在如何划分的问题上没有发言权,因此不需要根据能力创建阴影层次结构.

此外,因为派生类不能更改方法实现,所以它们不会破坏scratch()基类接口中方法的预期语义.

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