【问题标题】:Is it a good practice to unit test the Liskov Substitution principle's compliance?对 Liskov 替换原则的合规性进行单元测试是一种好习惯吗?
【发布时间】:2012-10-23 01:09:53
【问题描述】:

假设一个名为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 个解决方案(前两个解决方案已向所有团队的开发人员发送了 RULES 信号,并且必须被承认和保留,即使不是每个人都很好地掌握 Liskov 的概念)

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

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

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

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

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

我当然不想将我所有的具体类都声明为“final”:)

【问题讨论】:

  • 应用的LSP在吗?您没有将对象作为参数传递给任何函数,而是使用您在同一类中定义的对象。
  • 我说:“将它注入到一个共享的巨大异类 Sprinter 列表中,然后用‘你好,奇怪的行为!’来面对我!”因此,当任何客户端代码操作 @​​987654337@s 对象导致子类影响基类的预期行为时,当然会发生 LSP。我同意,帖子没有代码,因为它很容易推断出来。
  • 但这超出了单元测试的范围。

标签: java unit-testing inheritance liskov-substitution-principle


【解决方案1】:

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

【讨论】:

    【解决方案2】:

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

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

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

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

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

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

    【讨论】:

    • 不错的答案!谢谢 :) 所以它打破了彼得的替代原则? (什么都不明白的人;)我在开玩笑):)
    • LSP 讲前置条件和后置条件。前置条件的一个例子是传递给run()seconds 的值是一个整数。后置条件的一个例子是this.travelMeters 等于对象的速度值乘以给run() 的秒数。如果后置条件受到限制,使得 Sprinter 必须具有某个速度值,那么 LSP 会说 SprintGenius 在 LSP 意义上不再是 Sprinter 的子类,即使代码仍然可以编译。
    猜你喜欢
    • 2012-03-01
    • 1970-01-01
    • 2015-05-06
    • 2017-03-09
    • 2011-01-06
    • 1970-01-01
    • 2010-11-25
    • 1970-01-01
    • 2012-11-30
    相关资源
    最近更新 更多