2

假设一个名为 的类Sprinter

public class Sprinter {

    protected int travelMeters;

    public void run(int seconds) {
        this.travelMeters = 9 * seconds;
    }

    public int getTravelMeters(){
        return travelMeters;
    }
}

和一个SprintGenius类型继承Sprinter

class SprintGenius extends Sprinter {

    public void run(int seconds) {
        this.travelMeters = 10 * seconds;
    }
}

从逻辑上讲,必须创建 2 个单元测试类,每种类型一个。

Sprinter单元测试中,我最终会得到:

@Before
public void setUp() {
  Sprinter sprinter = new Sprinter();
}

public void testSprinterShouldRun90metersWithin10Seconds() {
  sprinter.run(10);
  assertEquals(sprinter.getTraveledMeters(),90);
}

SprintGenius单元测试中,我最终会得到:

@Before
public void setUp() {
  Sprinter sprinter = new SprintGenius();
}

public void testSprinterShouldRun100metersWithin10Seconds() {
  sprinter.run(10);
  assertEquals(sprinter.getTraveledMeters(),100);
}

在上面的两个测试中,我都会在 10 秒内测试行进的米数。

显然,这两个测试都是绿色的。

但是,违反里氏替换原则怎么办?

事实上,任何客户端代码都应该期望任何短跑运动员在 9 秒内跑完 10 米。

3 个解决方案(前两个解决方案向所有团队的开发人员发出了规则,即使不是每个人都能很好地掌握 Liskov 的概念,也必须承认并保留)

1) 在Sprinter课堂上,重复每个测试,但这次基于 aSprinter sprinter = new SuperGenius()并期望 90 米。=> 什么应该失败,这正是我们想要的!=> 防止违反 Liskov 原则。

2)在SprintGenius类中,始终基于完全相同的期望添加基于基类的每个测试的类似“克隆”。所以,如果你有 2 个不同的测试,我们最终会得到 4 个测试。2 将 Sprinter 声明为 aSprinter和 2 声明SprinterSprintGenius.

3)从不继承具体类(我想这是你阅读这篇文章的第一反应:)),如果合适的话,更喜欢组合!这样这个问题就不会发生了。

基于许多开发人员忽略 Liskov 原则并且经常试图从具体类继承而不是使用其他更好的方法(如组合或不同的继承层次结构)这一事实,防止违反 Liskov 替换原则的最佳实践是什么?

我不想因为开发人员从我的书面课程继承(没有告诉我..),将其注入到一个共享的巨大异构Sprinter列表列表中并以“你好,奇怪的行为!”而给我带来麻烦。和数小时的调试时间......

我当然不想宣布我所有的具体类“最终”:)

4

2 回答 2

4

单元测试是关于特定模块的测试,不能也不应该用于更广泛的事情。遵守 Liskov 替换原则是系统范围内的一个更广泛的问题,而不是模块范围内的问题。此外,它不是要在代码中测试的东西。这是一个纯粹的设计问题,与实现无关。我不认为自动工具可以强制执行 LSP。它应该在设计审查期间和稍后在代码审查期间处理(应检查是否符合设计)。

于 2012-11-02T11:24:19.263 回答
3

这并不违反 Liskov 替代原则的合规性。这是糟糕的设计。你的两个类在行为上没有不同,但在数据上。因此,您应该只有一个班级,并且客户应该期望任何短跑运动员的跑距离与其速度成正比

因此,您应该添加 speed 属性并拥有具有具体行为的单个类。

之后,您可以考虑创建具有新行为的真正扩展类并考虑测试。

有了这个速度参数,对于其他类型的跑步者,即使他们以不同的速度跑步,也不应该打破 Liskov 替换原则。

你的问题是这样的:我的类扩展一个人没有通过测试,因为我已经将一个人的名字从“彼得”改为“罗伯特”。

对于此类问题,这是一个不好的例子。举个恰当的例子,我认为是的,是退出测试它的好习惯,但这是一种极端防御性的方法。可能您可以利用给定的时间来更好地创建测试。此外,该测试将在很短的时间内过时,很难为新的子类添加测试以确保旧行为正常工作。

于 2012-11-02T11:30:47.300 回答