Wie vereinfache ich meine komplexen Stateful-Klassen und deren Tests?

9

Ich bin in einem verteilten Systemprojekt, das in Java geschrieben wurde, wo wir einige Klassen haben, die sehr komplexen realen Geschäftsobjekten entsprechen. Diese Objekte verfügen über viele Methoden, die Aktionen entsprechen, die der Benutzer (oder ein anderer Agent) auf diese Objekte anwenden kann. Infolgedessen wurden diese Klassen sehr komplex.

Der allgemeine Architekturansatz des Systems hat zu vielen Verhaltensweisen geführt, die sich auf wenige Klassen und viele mögliche Interaktionsszenarien konzentrieren.

Nehmen wir als Beispiel und um die Dinge einfach und klar zu halten, sagen wir, dass Roboter und Auto Klassen in meinem Projekt waren.

In der Roboterklasse hätte ich also viele Methoden im folgenden Muster:

  • Schlaf(); isSleepAvaliable ();
  • Erwachen(); isAwakeAvaliable ();
  • gehen (Richtung); isWalkAvaliable ();
  • schießen (Richtung); isShootAvaliable ();
  • turnOnAlert (); isTurnOnAlertAvailable ();
  • turnOffAlert (); isTurnOffAlertAvailable ();
  • aufladen(); isRechargeAvailable ();
  • ausschalten(); isPowerOffAvailable ();
  • stepInCar (Auto); isStepInCarAvailable ();
  • stepOutCar (Auto); isStepOutCarAvailable ();
  • Selbstzerstörung(); isSelfDestructAvailable ();
  • sterben(); isDieAvailable ();
  • ist am Leben(); ist wach(); isAlertOn (); getBatteryLevel (); getCurrentRidingCar (); getAmmo ();
  • ...

In der Autoklasse wäre es ähnlich:

  • anschalten(); isTurnOnAvaliable ();
  • abschalten(); isTurnOffAvaliable ();
  • gehen (Richtung); isWalkAvaliable ();
  • tanken(); isRefuelAvailable ();
  • Selbstzerstörung(); isSelfDestructAvailable ();
  • Absturz(); isCrashAvailable ();
  • isOperational (); isOn (); getFuelLevel (); getCurrentPassenger ();
  • ...

Jedes dieser Elemente (Roboter und Auto) ist als Zustandsmaschine implementiert, wobei einige Aktionen in einigen Zuständen möglich sind und andere nicht. Die Aktionen ändern den Status des Objekts. Die Aktionsmethoden werden ausgelöst, IllegalStateExceptionwenn sie in einem ungültigen Zustand aufgerufen werden, und die isXXXAvailable()Methoden geben an, ob die Aktion zu diesem Zeitpunkt möglich ist. Obwohl einige leicht aus dem Zustand abgeleitet werden können (z. B. im Schlafzustand ist Wach verfügbar), sind andere nicht (um zu schießen, muss es wach, lebendig, mit Munition sein und kein Auto fahren).

Darüber hinaus sind auch die Wechselwirkungen zwischen den Objekten komplex. ZB kann das Auto nur einen Roboterpassagier halten. Wenn also ein anderer versucht einzutreten, sollte eine Ausnahme ausgelöst werden. Wenn das Auto abstürzt, sollte der Passagier sterben; Wenn der Roboter in einem Fahrzeug tot ist, kann er nicht aussteigen, auch wenn das Auto selbst in Ordnung ist. Befindet sich der Roboter in einem Auto, kann er vor dem Aussteigen keinen anderen betreten. etc.

Das Ergebnis ist, wie ich bereits sagte, dass diese Klassen wirklich komplex wurden. Um die Sache noch schlimmer zu machen, gibt es Hunderte von möglichen Szenarien, in denen der Roboter und das Auto interagieren. Darüber hinaus muss ein Großteil dieser Logik auf entfernte Daten in anderen Systemen zugreifen. Das Ergebnis ist, dass Unit-Tests sehr schwierig wurden und wir viele Testprobleme haben, von denen eines das andere in einem Teufelskreis verursacht:

  • Die Testfall-Setups sind sehr komplex, da sie eine sehr komplexe Welt zum Trainieren erstellen müssen.
  • Die Anzahl der Tests ist enorm.
  • Die Ausführung der Testsuite dauert einige Stunden.
  • Unsere Testabdeckung ist sehr gering.
  • Der Testcode wird in der Regel Wochen oder Monate später als der von ihnen getestete Code oder überhaupt nicht geschrieben.
  • Viele Tests sind ebenfalls fehlerhaft, hauptsächlich weil sich die Anforderungen des getesteten Codes geändert haben.
  • Einige Szenarien sind so komplex, dass sie beim Setup während des Setups fehlschlagen (wir haben in jedem Test ein Timeout konfiguriert, im schlimmsten Fall 2 Minuten lang und selbst dieses Mal haben wir sichergestellt, dass es sich nicht um eine Endlosschleife handelt).
  • Fehler treten regelmäßig in der Produktionsumgebung auf.

Dieses Roboter- und Autoszenario ist eine grobe Vereinfachung dessen, was wir in der Realität haben. Diese Situation ist eindeutig nicht beherrschbar. Deshalb bitte ich um Hilfe und Vorschläge, um: 1, die Komplexität der Klassen zu reduzieren; 2. Vereinfachen Sie die Interaktionsszenarien zwischen meinen Objekten. 3. Reduzieren Sie die Testzeit und die Menge des zu testenden Codes.

EDIT:
Ich glaube, ich war nicht klar über die Zustandsmaschinen. Der Roboter selbst ist eine Zustandsmaschine mit den Zuständen "schlafen", "wach", "aufladen", "tot" usw. Das Auto ist eine andere Zustandsmaschine.

EDIT 2: Für den Fall, dass Sie neugierig sind, was mein System tatsächlich ist, sind die Klassen, die interagieren, Dinge wie Server, IPAddress, Disk, Backup, Benutzer, SoftwareLicense usw. Das Szenario Roboter und Auto ist nur ein Fall, den ich gefunden habe das wäre einfach genug, um mein Problem zu erklären.

Victor Stafusa
quelle
Haben Sie darüber nachgedacht, bei Code Review.SE nachzufragen ? Abgesehen davon würde ich für Design wie das Ihre anfangen, über das Refactoring von Extract Class nachzudenken
Mücke
Ich habe über Code Review nachgedacht, aber das ist nicht der richtige Ort. Das Hauptproblem liegt nicht im Code selbst, sondern im allgemeinen Architekturansatz des Systems, der zu vielen Verhaltensweisen führt, die sich auf wenige Klassen und viele mögliche Interaktionsszenarien konzentrieren.
Victor Stafusa
@gnat Können Sie ein Beispiel dafür geben, wie ich die Extraktklasse in dem gegebenen Roboter- und Autoszenario implementieren würde?
Victor Stafusa
Ich würde Auto-bezogene Sachen aus Robot in eine separate Klasse extrahieren. Ich würde auch alle Methoden in Bezug auf Schlaf + Wach in eine spezielle Klasse extrahieren. Andere "Kandidaten", die eine Extraktion verdienen, sind Power + Recharge-Methoden und bewegungsbezogene Dinge. Hinweis: Da dies ein Refactoring ist, sollte die externe API für den Roboter wahrscheinlich erhalten bleiben. In der ersten Phase würde ich nur Interna ändern. BTDTGTTS
Mücke
Dies ist keine Frage zur Codeüberprüfung - Architektur ist dort nicht zum Thema.
Michael K

Antworten:

8

Das State- Entwurfsmuster kann hilfreich sein, wenn Sie es nicht bereits verwenden.

Die Kernidee ist , dass Sie eine innere Klasse für jeden einzelnen Staat zu schaffen - so wird Ihre Beispiel fortzusetzen SleepingRobot, AwakeRobot, RechargingRobotund DeadRobotalle Klassen sein würde, eine gemeinsame Schnittstelle implementieren.

Methoden für die RobotKlasse (wie sleep()und isSleepAvaliable()) haben einfache Implementierungen, die an die aktuelle innere Klasse delegieren.

Zustandsänderungen werden implementiert, indem die aktuelle innere Klasse gegen eine andere ausgetauscht wird.

Der Vorteil dieses Ansatzes besteht darin, dass jede der Zustandsklassen erheblich einfacher ist (da sie nur einen möglichen Zustand darstellt) und unabhängig getestet werden kann. Abhängig von Ihrer Implementierungssprache (nicht angegeben) müssen Sie möglicherweise immer noch alles in derselben Datei haben, oder Sie können Dinge in kleinere Quelldateien aufteilen.

Bevan
quelle
Ich benutze Java.
Victor Stafusa
Guter Vorschlag. Auf diese Weise hat jede Implementierung einen klaren Fokus, der einzeln getestet werden kann, ohne dass eine Junit-Klasse mit 2.000 Zeilen alle Zustände gleichzeitig testet.
OliverS
3

Ich kenne Ihren Code nicht, aber am Beispiel der "Schlaf" -Methode gehe ich davon aus, dass es sich um etwas handelt, das dem folgenden "simplen" Code ähnelt:

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

Ich denke, man muss zwischen Integrationstests und Unit-Tests unterscheiden . Es ist sicherlich eine große Aufgabe, einen Test zu schreiben, der Ihren gesamten Maschinenzustand durchläuft. Das Schreiben kleinerer Komponententests, mit denen geprüft wird, ob Ihre Schlafmethode ordnungsgemäß funktioniert, ist einfacher. Zu diesem Zeitpunkt müssen Sie nicht wissen, ob der Maschinenzustand ordnungsgemäß aktualisiert wurde oder ob das "Auto" korrekt auf die Tatsache reagiert hat, dass der Maschinenzustand vom "Roboter" aktualisiert wurde ... usw. Sie erhalten ihn.

Angesichts des obigen Codes würde ich das "machineState" -Objekt verspotten und mein erster Test wäre:

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());
 }
}

Meine persönliche Meinung ist, dass das Schreiben solcher kleinen Unit-Tests das erste sein sollte, was zu tun ist. Das hast du geschrieben:

Die Testfall-Setups sind sehr komplex, da sie eine sehr komplexe Welt zum Trainieren erstellen müssen.

Das Ausführen dieser kleinen Tests sollte sehr schnell sein, und Sie sollten im Voraus nichts zu initialisieren haben, wie Ihre "komplexe Welt". Wenn es sich beispielsweise um eine Anwendung handelt, die auf einem IOC-Container basiert (z. B. Spring), sollten Sie den Kontext während Unit-Tests nicht initialisieren müssen.

Nachdem Sie einen guten Prozentsatz Ihres komplexen Codes mit Komponententests abgedeckt haben, können Sie mit dem Erstellen zeitaufwändigerer und komplexerer Integrationstests beginnen.

Schließlich kann dies unabhängig davon erfolgen, ob Ihr Code komplex ist (wie Sie bereits sagten) oder nachdem Sie ihn überarbeitet haben.

Jalayn
quelle
Ich glaube, mir war die Zustandsmaschine nicht klar. Der Roboter selbst ist eine Zustandsmaschine mit den Zuständen "schlafen", "wach", "aufladen", "tot" usw. Das Auto ist eine andere Zustandsmaschine.
Victor Stafusa
@ Victor OK, zögern Sie nicht, meinen Beispielcode zu korrigieren, wenn Sie möchten. Wenn Sie mir nichts anderes sagen, denke ich, dass mein Punkt zu Unit-Tests immer noch gültig ist, ich hoffe es zumindest.
Jalayn
Ich habe das Beispiel korrigiert. Ich habe kein Privileg, es leicht sichtbar zu machen, daher muss es zuerst einer Peer-Review unterzogen werden. Ihr Kommentar ist hilfreich.
Victor Stafusa
2

Ich habe den Abschnitt "Ursprung" des Wikipedia-Artikels über das Prinzip der Schnittstellentrennung gelesen und wurde an diese Frage erinnert.

Ich werde den Artikel zitieren. Das Problem: "... eine Hauptberufsklasse ... eine Fettklasse mit einer Vielzahl von Methoden, die für eine Vielzahl unterschiedlicher Kunden spezifisch sind." Die Lösung: "... eine Schicht von Schnittstellen zwischen der Jobklasse und all ihren Clients ..."

Ihr Problem scheint eine Permutation desjenigen zu sein, das Xerox hatte. Anstelle einer Fettklasse haben Sie zwei, und diese beiden Fettklassen sprechen miteinander anstatt mit vielen Kunden.

Können Sie die Methoden nach Interaktionstyp gruppieren und dann für jeden Typ eine Schnittstellenklasse erstellen? Zum Beispiel: RobotPowerInterface-, RobotNavigationInterface-, RobotAlarmInterface-Klassen?

Jeff
quelle