複雑なステートフルクラスとそのテストを簡略化するにはどうすればよいですか?


9

私はJavaで書かれた分散システムプロジェクトに参加しています。このプロジェクトには、非常に複雑な現実のビジネスオブジェクトに対応するクラスがいくつかあります。これらのオブジェクトには、ユーザー(または他のエージェント)がそのオブジェクトに適用できるアクションに対応する多くのメソッドがあります。その結果、これらのクラスは非常に複雑になりました。

システムの一般的なアーキテクチャアプローチにより、多くの動作がいくつかのクラスに集中し、多くの可能な相互作用シナリオが発生します。

例として、わかりやすくするために、ロボットと車が私のプロジェクトのクラスであったとしましょう。

したがって、Robotクラスには、次のパターンで多くのメソッドが含まれます。

  • 睡眠(); isSleepAvaliable();
  • 起きている(); isAwakeAvaliable();
  • walk(方向); isWalkAvaliable();
  • シュート(方向); isShootAvaliable();
  • turnOnAlert(); isTurnOnAlertAvailable();
  • turnOffAlert(); isTurnOffAlertAvailable();
  • recharge(); isRechargeAvailable();
  • 電源を切る(); isPowerOffAvailable();
  • stepInCar(Car); isStepInCarAvailable();
  • stepOutCar(Car); isStepOutCarAvailable();
  • 自己破壊(); isSelfDestructAvailable();
  • die(); isDieAvailable();
  • 生きている(); isAwake(); isAlertOn(); getBatteryLevel(); getCurrentRidingCar(); getAmmo();
  • ...

Carクラスでも同様です。

  • オンにする(); isTurnOnAvaliable();
  • 消す(); isTurnOffAvaliable();
  • walk(方向); isWalkAvaliable();
  • refuel(); isRefuelAvailable();
  • 自己破壊(); isSelfDestructAvailable();
  • クラッシュ(); isCrashAvailable();
  • isOperational(); isOn(); getFuelLevel(); getCurrentPassenger();
  • ...

これら(ロボットと車)はそれぞれ状態マシンとして実装されており、一部のアクションは一部の状態で可能であり、一部のアクションは不可能です。アクションはオブジェクトの状態を変更します。アクションメソッドIllegalStateExceptionは、無効な状態で呼び出されたときにスローされ、isXXXAvailable()メソッドはその時点でアクションが可能かどうかを通知します。状態から簡単に推測できるものもありますが(たとえば、眠っている状態では、目が覚めている状態です)、そうでないものもあります(発射するには、起きていて、生きていて、弾薬があり、車に乗っていない)。

さらに、オブジェクト間の相互作用も複雑です。たとえば、車はロボットの乗客を1人しか保持できないため、別の人が乗ろうとすると、例外がスローされます。車がクラッシュした場合、乗客は死ぬはずです。ロボットが車内で死んでいる場合、たとえ車自体に問題がなくても、ロボットは外に出られません。ロボットが車の中にある場合、彼は脱出する前に別のロボットに入ることはできません。等

その結果、すでに述べたように、これらのクラスは非常に複雑になりました。さらに悪いことに、ロボットと車が相互作用するシナリオは何百とあります。さらに、そのロジックの多くは、他のシステムのリモートデータにアクセスする必要があります。その結果、単体テストが非常に難しくなり、テストの問題がたくさんあります。

  • テストケースのセットアップは、非常に複雑な世界を作成して実行する必要があるため、非常に複雑です。
  • テストの数は膨大です。
  • テストスイートの実行には数時間かかります。
  • 私たちのテストカバレッジは非常に低いです。
  • テストコードは、テストするコードよりも数週間または数か月遅れて記述されるか、まったく記述されない傾向があります。
  • テストされたコードの要件が変更されたことが主な原因です。
  • 一部のシナリオは非常に複雑で、セットアップ中にタイムアウトで失敗します(各テストでタイムアウトを構成しましたが、最悪の場合、2分間、さらにこの時間タイムアウトしても、無限ループではないことを確認しました)。
  • バグは定期的に本番環境に侵入します。

そのロボットと車のシナリオは、私たちが実際に持っているものを大幅に単純化しすぎています。明らかに、この状況は管理できません。ですから、私はヘルプと提案を求めています:1、クラスの複雑さを減らします。2.オブジェクト間の相互作用シナリオを簡素化します。3.テスト時間とテストするコードの量を減らします。

編集:
私は状態機械について明確ではなかったと思います。ロボットはそれ自体がステートマシンであり、「スリープ」、「覚醒」、「再充電」、「デッド」などの状態を持ちます。車は別のステートマシンです。

編集2:私のシステムが実際に何であるかに興味がある場合、相互作用するクラスは、サーバー、IPアドレス、ディスク、バックアップ、ユーザー、ソフトウェアライセンスなどです。ロボットと車のシナリオは、私が見つけた単なるケースですそれは私の問題を説明するのに十分簡単です。


Code Review.SEで質問することを検討しましたか?それ以外は、あなたのようなデザインのために、Extract Classの種類のリファクタリングについて考え始めます
gnat

私はコードレビューを検討しましたが、それは適切な場所ではありません。主な問題はコード自体ではなく、システムの一般的なアーキテクチャアプローチにあります。このアプローチは、いくつかのクラスに集中した多くの動作と、多くの可能な相互作用シナリオにつながります。
ビクターStafusa

@gnat特定のロボットと車のシナリオで抽出クラスを実装する方法の例を提供できますか?
ビクターStafusa

ロボットから車関連のものを別のクラスに抽出します。また、sleep + awakeに関連するすべてのメソッドを専用クラスに抽出します。抽出に値するように見える他の「候補者」は、パワー+リチャージ方法、運動関連のものです。などこれはリファクタリングであるため、ロボットの外部APIはおそらくとどまるはずです。最初の段階では、内部のみを変更します。BTDTGTTS
ブヨ

これはコードレビューの質問ではありません-アーキテクチャはトピックから外れています。
Michael K

回答:


8

国家あなたはすでにそれを使用していない場合は、デザインパターンは、使用のかもしれません。

ので、あなたの例を続けるために、 -核となるアイデアは、各個別の状態のための内部クラスを作成することでSleepingRobotAwakeRobotRechargingRobotおよびDeadRobotすべての共通のインターフェースを実装、クラスだろう。

Robotクラスのメソッド(sleep()およびなどisSleepAvaliable())には、現在の内部クラスに委譲する単純な実装があります。

状態の変更は、現在の内部クラスを別の内部クラスと交換することによって実装されます。

このアプローチの利点は、状態クラスのそれぞれが(可能な1つの状態のみを表すため)劇的に単純になり、独立してテストできることです。実装言語(指定されていない)によっては、すべてを同じファイルに含めることに制限されている場合や、より小さなソースファイルに分割できる場合があります。


私はjavaを使用しています。
Victor Stafusa

良い提案。このように、各実装には明確な焦点があります。これは、2.000行のjunitクラスがすべての状態を同時にテストすることなく、個別にテストできます。
OliverS 2012

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 Stafusa

@Victor OK。必要に応じて、サンプルコードを自由に修正してください。特に断りのない限り、ユニットテストに関する私の主張は今でも有効だと思います。
Jalayn、2012

例を修正しました。私はそれをすぐに見えるようにする特権を持っていないので、最初にピアレビューを受ける必要があります。あなたのコメントは役に立ちます。
ビクターStafusa

2

ウィキペディアのインターフェイス分離原理に関する記事「起源」セクションを読んでいたところ、この質問を思い出しました。

記事を引用します。問題:「... 1つのメインJobクラス....さまざまな異なるクライアントに固有の多数のメソッドを持つファットクラス」解決策:「... Jobクラスとそのすべてのクライアント間のインターフェースのレイヤー...」

あなたの問題はゼロックスが持っていたものの順列のようです。1つのファットクラスの代わりに2つのファットクラスがあり、これらの2つのファットクラスは多くのクライアントの代わりに互いに話し合っています。

相互作用のタイプごとにメソッドをグループ化してから、タイプごとにインターフェースクラスを作成できますか?例:RobotPowerInterface、RobotNavigationInterface、RobotAlarmInterfaceクラス?

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.