如何简化我复杂的有状态类及其测试?


9

我在用Java编写的分布式系统项目中,其中有一些类对应于非常复杂的现实世界业务对象。这些对象具有许多与用户(或某些其他代理)可以应用于该对象的操作相对应的方法。结果,这些类变得非常复杂。

系统通用体系结构方法导致许多行为集中在少数几个类上,并且涉及许多可能的交互方案。

举个例子,为了使事情变得简单明了,假设机器人和汽车是我项目中的类。

因此,在Robot类中,我将采用以下模式提供许多方法:

  • 睡觉(); isSleepAvaliable();
  • 苏醒(); isAwakeAvaliable();
  • 步行(方向);isWalkAvaliable();
  • 射击(方向);isShootAvaliable();
  • turnOnAlert(); isTurnOnAlertAvailable();
  • turnOffAlert(); isTurnOffAlertAvailable();
  • recharge(); isRechargeAvailable();
  • powerOff(); isPowerOffAvailable();
  • stepInCar(Car); isStepInCarAvailable();
  • stepOutCar(Car); isStepOutCarAvailable();
  • 自毁(); isSelfDestructAvailable();
  • 死(); isDieAvailable();
  • 活着(); isAwake(); isAlertOn(); getBatteryLevel(); getCurrentRidingCar(); getAmmo();
  • ...

在Car类中,它将类似于:

  • 打开(); isTurnOnAvaliable();
  • 关掉(); isTurnOffAvaliable();
  • 步行(方向);isWalkAvaliable();
  • 加油(); isRefuelAvailable();
  • 自毁(); isSelfDestructAvailable();
  • crash(); isCrashAvailable();
  • isOperational(); isOn(); getFuelLevel(); getCurrentPassenger();
  • ...

这些(机器人和汽车)中的每一个都实现为状态机,在某些状态下某些动作是可能的,而在某些状态下则不可能。这些动作将更改对象的状态。IllegalStateException当在无效状态下调用时,actions方法将引发,并且这些isXXXAvailable()方法将告知当时是否可以执行该操作。尽管可以从状态轻松推断出某些状态(例如,在睡眠状态下可以清醒),但是某些状态则不行(要开枪,它必须醒着,还活着,有弹药并且不开车)。

此外,对象之间的交互也很复杂。例如,汽车只能容纳一名机器人乘客,因此如果另一名乘客试图进入,则应抛出异常;如果汽车撞车,乘客应该死亡;如果机器人死于车内,即使汽车本身没问题,他也无法走出。如果机器人在汽车内,则他不能在下车之前进入另一个机器人。等等

正如我已经说过的那样,这些类变得非常复杂。更糟糕的是,机器人与汽车互动时有数百种可能的情况。此外,许多逻辑确实需要访问其他系统中的远程数据。结果是,单元测试变得非常困难,并且我们遇到了很多测试问题,一个导致另一个问题处于恶性循环:

  • 测试用例的设置非常复杂,因为它们需要创建一个非常复杂的世界来执行。
  • 测试数量巨大。
  • 测试套件需要几个小时才能运行。
  • 我们的测试覆盖率很低。
  • 测试代码往往比其测试的代码晚几周或几个月编写,或者根本不会编写。
  • 许多测试也被破坏了,主要是因为被测试代码的需求已更改。
  • 某些情况是如此复杂,以至于在设置期间会超时(我们在每个测试中都配置了超时,在最坏的情况下,超时时间为2分钟,即使这段时间超时,我们也确保它不是无限循环)。
  • 错误通常会潜入生产环境。

机器人和汽车的场景是对我们现实中的事物的过度简化。显然,这种情况是无法控制的。因此,我在寻求帮助和建议:1,降低类的复杂性;2.简化对象之间的交互方案;3.减少测试时间和要测试的代码量。

编辑:
我想我还不清楚状态机。机器人本身就是状态机,状态为“睡眠”,“清醒”,“正在充电”,“死亡”等。汽车是另一种状态机。

编辑2:如果您对我的系统实际是一个很好的情况感到好奇,则交互的类是诸如Server,IPAddress,Disk,Backup,User,SoftwareLicense等之类的东西。Robot和Car场景只是我发现的一个案例那将足以解释我的问题。


您是否考虑过在Code Review.SE上询问?除此之外,对于像您这样的设计,我将开始考虑Extract类的重构
gnat 2012年

我考虑过代码审查,但这不是正确的地方。主要问题不在于代码本身,而在于系统通用体系结构方法,该方法导致许多行为集中在少数几个类和许多可能的交互方案上。
维克多·斯塔夫萨

@gnat您能否提供一个示例,说明如何在给定的机器人和汽车场景中实现提取类?
维克多·斯塔夫萨

我将从“机器人”中提取与汽车相关的内容到单独的类中。我还将所有与sleep + awake相关的方法提取到一个专用的类中。其他似乎值得提取的“候选人”是电源+充电方法,与运动有关的东西。等等,请注意,由于这是重构,可能会保留用于机器人的外部API。在第一阶段,我只修改内部结构。BTDTGTTS
蚊蚋

这不是代码审查问题-那里的体系结构是题外话。
Michael K

Answers:


8

的设计模式可能是使用的,如果你不是已经在使用它。

其核心思想是您创建的每个不同状态的内部类-所以继续你的榜样,SleepingRobotAwakeRobotRechargingRobot并且DeadRobot都将是类,实现一个通用接口。

Robot类上的方法(如sleep()isSleepAvaliable())具有委托给当前内部类的简单实现。

通过将当前内部类换成其他内部类来实现状态更改。

这种方法的优点是每个状态类都非常简单(因为它仅表示一种可能的状态),并且可以独立测试。根据您的实现语言(未指定),您可能仍然受限于将所有内容都放在同一文件中,或者可以将内容拆分成较小的源文件。


我正在使用Java。
维克多·斯塔夫萨

好建议。这样,每个实现都有一个明确的焦点,可以单独进行测试,而无需使用2.000行junit类同时测试所有状态。
OliverS'2

3

我不知道您的代码,但是以“ sleep”方法为例,我假设它与以下“简单”代码类似:

public void sleep() {
 if(!dead && awake) {
  sleeping = true;
  awake = false;
  this.updateState(SLEEPING);
 }
 throw new IllegalArgumentException("robot is either dead or not awake");
}

我认为您必须在集成测试单元测试之间有所作为。编写贯穿整个机器状态运行的测试无疑是一项艰巨的任务。编写较小的单元测试来测试您的睡眠方法是否正常工作将变得更加容易。此时,您不必知道机器状态是否已正确更新,或者“汽车”是否正确响应了“机器人”已更新机器状态的事实……等等。

给定上面的代码,我将模拟 “ machineState”对象,而我的第一个测试将是:

testSleep_dead() {
 robot.dead = true;
 robot.awake = false;
 robot.setState(AWAKE);
 try {
  robot.sleep();
  fail("should have got an exception");
 } catch(Exception e) {
  assertTrue(e instanceof IllegalArgumentException);
  assertEquals("robot is either dead or not awake", e.getMessage());
 }
}

我个人认为编写此类小型单元测试应该是第一件事。您写道:

测试用例的设置非常复杂,因为它们需要创建一个非常复杂的世界来执行。

运行这些小型测试应该非常快,并且您不应该像“复杂世界”那样预先进行任何初始化。例如,如果它是基于IOC容器(例如Spring)的应用程序,则在单元测试期间无需初始化上下文。

在使用单元测试覆盖了相当一部分复杂代码之后,您可能会开始构建更耗时且更复杂的集成测试。

最后,无论您的代码是复杂的(如您现在所说的那样),还是在重构之后,都可以这样做。


我想我还不清楚状态机。机器人本身就是状态机,状态为“睡眠”,“清醒”,“正在充电”,“死亡”等。汽车是另一种状态机。
维克多·斯塔夫萨

@Victor OK,如果需要,可以随时更正我的示例代码。除非您另有说明,否则我认为我对单元测试的观点仍然有效,至少希望如此。
Jalayn

我纠正了这个例子。我没有使它易于显示的特权,因此必须首先经过同行评审。您的评论很有帮助。
维克多·斯塔夫萨

2

我正在阅读Wikipedia上有关接口隔离原理的文章“来源”部分,这使我想起了这个问题。

我将引用这篇文章。问题是:“ ...一个主要的Job类。...一个胖类,其中包含针对各种不同客户的大量方法。” 解决方案:“ ...作业类与其所有客户端之间的接口层...”

您的问题似乎是施乐公司的一个替代品。而不是一个胖类,您有两个,而这两个胖类是在互相交谈,而不是很多客户。

您可以按交互类型对方法进行分组,然后为每种类型创建一个接口类吗?例如:RobotPowerInterface,RobotNavigationInterface,RobotAlarmInterface类?

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.