【发布时间】: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