Comment simplifier mes classes complexes avec état et leurs tests?


9

Je suis dans un projet de système distribué écrit en java où nous avons des classes qui correspondent à des objets métier très complexes. Ces objets ont de nombreuses méthodes correspondant aux actions que l'utilisateur (ou un autre agent) peut appliquer à ces objets. En conséquence, ces classes sont devenues très complexes.

L'approche de l'architecture générale du système a conduit à de nombreux comportements concentrés sur quelques classes et à de nombreux scénarios d'interaction possibles.

À titre d'exemple et pour que les choses soient simples et claires, disons que Robot et Car étaient des classes dans mon projet.

Donc, dans la classe Robot, j'aurais beaucoup de méthodes dans le modèle suivant:

  • dormir(); isSleepAvaliable ();
  • éveillé(); isAwakeAvaliable ();
  • marche (Direction); isWalkAvaliable ();
  • tirer (Direction); isShootAvaliable ();
  • turnOnAlert (); isTurnOnAlertAvailable ();
  • turnOffAlert (); isTurnOffAlertAvailable ();
  • recharger(); isRechargeAvailable ();
  • éteindre(); isPowerOffAvailable ();
  • stepInCar (Voiture); isStepInCarAvailable ();
  • stepOutCar (Voiture); isStepOutCarAvailable ();
  • auto-destruction(); isSelfDestructAvailable ();
  • mourir(); isDieAvailable ();
  • est vivant(); est réveillé(); isAlertOn (); getBatteryLevel (); getCurrentRidingCar (); getAmmo ();
  • ...

Dans la classe Voiture, ce serait similaire:

  • allumer(); isTurnOnAvaliable ();
  • éteindre(); isTurnOffAvaliable ();
  • marche (Direction); isWalkAvaliable ();
  • ravitailler(); isRefuelAvailable ();
  • auto-destruction(); isSelfDestructAvailable ();
  • crash(); isCrashAvailable ();
  • isOperational (); est sur(); getFuelLevel (); getCurrentPassenger ();
  • ...

Chacun d'entre eux (Robot et voiture) est implémenté comme une machine à états, où certaines actions sont possibles dans certains états et d'autres non. Les actions modifient l'état de l'objet. Les méthodes d'actions sont lancées IllegalStateExceptionlorsqu'elles sont appelées dans un état non valide et les isXXXAvailable()méthodes indiquent si l'action est possible à ce moment-là. Bien que certains soient facilement déductibles de l'état (par exemple, dans l'état de sommeil, l'éveil est disponible), certains ne le sont pas (pour tirer, il doit être éveillé, vivant, avoir des munitions et ne pas conduire de voiture).

De plus, les interactions entre les objets sont également complexes. Par exemple, la voiture ne peut contenir qu'un seul passager robot, donc si un autre essaie d'entrer, une exception doit être levée; Si la voiture s'écrase, le passager doit mourir; Si le robot est mort à l'intérieur d'un véhicule, il ne peut pas sortir, même si la voiture elle-même est ok; Si le robot est à l'intérieur d'une voiture, il ne peut pas en entrer une autre avant de sortir; etc.

Le résultat de cela, comme je l'ai déjà dit, ces classes sont devenues vraiment complexes. Pour aggraver les choses, il existe des centaines de scénarios possibles lorsque le robot et la voiture interagissent. En outre, une grande partie de cette logique doit accéder à des données distantes dans d'autres systèmes. Le résultat est que les tests unitaires sont devenus très difficiles et nous avons beaucoup de problèmes de test, l'un causant l'autre dans un cercle vicieux:

  • Les configurations de testcases sont très complexes, car elles doivent créer un monde considérablement complexe à exercer.
  • Le nombre de tests est énorme.
  • La suite de tests prend quelques heures à s'exécuter.
  • Notre couverture de test est très faible.
  • Le code de test a tendance à être écrit des semaines ou des mois plus tard que le code qu'ils testent, ou jamais du tout.
  • De nombreux tests sont également interrompus, principalement parce que les exigences du code testé ont changé.
  • Certains scénarios sont si complexes qu'ils échouent lors de la temporisation lors de la configuration (nous avons configuré une temporisation dans chaque test, dans le pire des cas 2 minutes et même cette fois ils dépassent le délai, nous nous sommes assurés qu'il ne s'agit pas d'une boucle infinie).
  • Les bugs se glissent régulièrement dans l'environnement de production.

Ce scénario Robot and Car est une simplification grossière de ce que nous avons en réalité. De toute évidence, cette situation n'est pas gérable. Donc, je demande de l'aide et des suggestions pour: 1, réduire la complexité des classes; 2. Simplifier les scénarios d'interactions entre mes objets; 3. Réduisez la durée du test et la quantité de code à tester.

EDIT:
Je pense que je n'étais pas clair sur les machines d'état. le Robot est lui-même une machine d'état, avec des états "endormi", "éveillé", "en recharge", "mort", etc. La voiture est une autre machine d'état.

EDIT 2: Dans le cas où vous êtes curieux de savoir ce qu'est réellement mon système, les classes qui interagissent sont des choses comme Server, IPAddress, Disk, Backup, User, SoftwareLicense, etc. Le scénario Robot and Car est juste un cas que j'ai trouvé ce serait assez simple pour expliquer mon problème.


avez-vous envisagé de demander à Code Review.SE ? Autre que cela, pour la conception comme le vôtre je commence à penser à Extract Class type refactorisation
moucheron

J'ai envisagé la révision du code, mais ce n'est pas le bon endroit. Le problème principal n'est pas dans le code lui-même, le problème réside dans l'approche de l'architecture générale du système qui conduit à de nombreux comportements concentrés sur quelques classes et à de nombreux scénarios d'interaction possibles.
Victor Stafusa

@gnat Pouvez-vous fournir un exemple de la façon dont j'implémenterais Extraire la classe dans le scénario Robot et voiture donné?
Victor Stafusa

J'extrais des trucs liés à la voiture de Robot dans une classe séparée. Je voudrais également extraire toutes les méthodes liées à sleep + awake dans une classe dédiée. D'autres «candidats» qui semblent mériter d'être extraits sont les méthodes d'alimentation + recharge, les trucs liés au mouvement. Etc. Notez qu'il s'agit d'un refactoring, l'API externe pour le robot devrait probablement rester; au premier stade, je ne modifierais que les internes. BTDTGTTS
gnat

Ce n'est pas une question de révision de code - l'architecture y est hors sujet.
Michael K

Réponses:


8

Le modèle de conception d' état peut être utile si vous ne l'utilisez pas déjà.

L'idée de base est que vous créez une classe interne pour chaque état distinct - à continuer votre exemple, SleepingRobot, AwakeRobot, RechargingRobotet DeadRobotseraient tous des cours, la mise en œuvre d' une interface commune.

Les méthodes de la Robotclasse (comme sleep()et isSleepAvaliable()) ont des implémentations simples qui se délèguent à la classe interne actuelle.

Les changements d'état sont implémentés en remplaçant la classe interne actuelle par une autre.

L'avantage de cette approche est que chacune des classes d'état est considérablement plus simple (car elle ne représente qu'un seul état possible) et peut être testée indépendamment. Selon votre langage d'implémentation (non spécifié), vous pouvez toujours être contraint d'avoir tout dans le même fichier, ou vous pouvez être en mesure de diviser les choses en fichiers source plus petits.


J'utilise java.
Victor Stafusa

Bonne suggestion. De cette façon, chaque implémentation a un objectif clair qui peut être testé individuellement sans qu'une classe de jonction de 2 000 lignes teste tous les états simultanément.
OliverS

3

Je ne connais pas votre code, mais en prenant l'exemple de la méthode "sleep", je suppose que c'est quelque chose qui ressemble au code "simpliste" suivant:

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

Je pense qu'il faut faire la différence entre les tests d'intégration et les tests unitaires . L'écriture d'un test qui s'exécute sur l'ensemble de l'état de votre machine est certainement une grosse tâche. Il est plus facile d'écrire des tests unitaires plus petits qui testent si votre méthode de sommeil fonctionne correctement. À ce stade, vous n'avez pas à savoir si l'état de la machine a été correctement mis à jour ou si la "voiture" a correctement répondu au fait que l'état de la machine a été mis à jour par le "robot" ... etc. vous l'obtenez.

Étant donné le code ci-dessus, je me moquerais de l'objet "machineState" et mon premier test serait:

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

Mon opinion personnelle est que l'écriture de tests unitaires aussi petits devrait être la première chose à faire. Vous avez écrit cela:

Les configurations de testcases sont très complexes, car elles doivent créer un monde considérablement complexe à exercer.

L'exécution de ces petits tests devrait être très rapide, et vous ne devriez rien avoir à initialiser au préalable comme votre "monde complexe". Par exemple, s'il s'agit d'une application basée sur un conteneur IOC (disons Spring), vous ne devriez pas avoir besoin d'initialiser le contexte pendant les tests unitaires.

Après avoir couvert un bon pourcentage de votre code complexe avec des tests unitaires, vous pouvez commencer à créer des tests d'intégration plus longs et plus complexes.

Enfin, cela peut être fait que votre code soit complexe (comme vous l'avez dit maintenant), ou après avoir refactorisé.


Je pense que je n'étais pas clair sur les machines d'état. le Robot est lui-même une machine d'état, avec des états "endormi", "éveillé", "en recharge", "mort", etc. La voiture est une autre machine d'état.
Victor Stafusa

@Victor OK, n'hésitez pas à corriger mon exemple de code si vous le souhaitez. À moins que vous ne me disiez le contraire, je pense que mon point sur les tests unitaires est toujours valable, j'espère au moins.
Jalayn

J'ai corrigé l'exemple. Je n'ai pas le privilège de le rendre facilement visible, il doit donc être évalué par des pairs en premier. Votre commentaire est utile.
Victor Stafusa

2

Je lisais la section "Origine" de l'article de Wikipédia sur le principe de ségrégation d'interface , et cela m'a rappelé cette question.

Je citerai l'article. Le problème: "... une classe d'emplois principale ... une classe grasse avec une multitude de méthodes spécifiques à une variété de clients différents." La solution: "... une couche d'interfaces entre la classe Job et tous ses clients ..."

Votre problème semble être une permutation de celui de Xerox. Au lieu d'une classe de graisse, vous en avez deux, et ces deux classes de graisse se parlent au lieu de beaucoup de clients.

Pouvez-vous regrouper les méthodes par type d'interaction, puis créer une classe d'interface pour chaque type? Par exemple: RobotPowerInterface, RobotNavigationInterface, RobotAlarmInterface classes?

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.