9) Tell Don't Ask

Step 9 : Tell Don't Ask

Le code des Use Cases ressemble pour le moment furieusement à du code procédural en :

  • interrogeant des objets

  • prenant des décisions basées sur l'état de ces objets

Tell Don't Ask

Voici un exemple avec un Use Case existant :

Nous allons encapsuler la prise de décision au niveau du Domain et faire en sorte que les Use Cases respectent le principe Tell Don't Ask :

  • Prendre du temps pour comprendre ce qu'est le principe Tell Don't Ask

  • Encapsuler le code Business des Use Cases dans le Domain

  • Revoir l'encapsulation des objets afin de préserver l'état du Domain

    • Rendre impossible de représenter un état invalide

    • Avoir des objets métiers porteurs de sens

Refactorer le Use Case : ReprendreLaPartie

  • On commence par extraire le contenu business du Use Case

    • Refactor -> Extract -> Extract Method

Extract Method
  • Nous devons passer la fonction _timeProvider en paramètre de la méthode

  • Nous pouvons maintenant déplacer la méthode dans la classe PartieDeChasse

    • Refactor -> Move

  • En déplaçant cette méthode dans le Domain, un test d'Architecture échoue :

    • Les exceptions lancées ne sont effectivement pas au sein du Domain

Broken Architecture Rule
  • Nous devons déplacer ces exceptions métiers au sein du Domain

Move exceptions

Découvertes

  • On continue ce refactoring pour chaque Use Case et faisons quelques "découvertes" :

    • 1 vérification manquante

  • De la duplication de code dans chaque Use Case :

  • Les méthodes Tirer et TirerSurUneGalinette contiennent du code dupliqué

    • Les appels au Save du repository se font dès qu'on ajoute un événement dans la liste d'events (même si une exception est lancée)

Refactorer le Domain

  • Après les différents refactorings voici l'état de la classe PartieDeChasse :

Supprimer les constructeurs (utiliser la Factory)

  • Nous sommes couverts par les tests, nous allons pouvoir nous amuser en terme de refactoring 😊

    • On commence par le feedback fourni par notre IDE sur les constructeurs

Make ctors private
  • Nous n'avons plus qu'un seul constructeur public :

  • Le constructeur est appelé par notre Test Data Builder :

  • Nous voulons forcer l'instantiation de cette classe par sa factory afin de ne plus pouvoir instancié une PartieDeChasse dans un état invalide

    • Ex : Une partie de chasse démarre avec 0 galinettes sur le terrain et des chasseurs sans balles...

    • Nous allons donc faire appel à la Factory Method plutôt qu'à 1 constructeur

  • En effectuant ce refactoring, nous avons 22 tests qui échouent...

22 failing tests
  • Pourquoi ? on ne set plus les Events ni le Status à l'instantiation

  • On va faire en sorte d'avancer en mettant en place une solution transitoire

  • Nous n'avons plus que 7 tests qui échouent

    • Ils échouent car l'état des chasseurs n'est pas bon...

    • On ne set pas les galinettes tuées

  • On va simuler le fait que le chasseur a tué des galinettes en appelant les méthodes du Domain

  • En continuant à fixer les tests, on identifie des tests qui ne sont pas consistants

  • On itère sur ce test :

  • D'autres tests posent problème dû à une mauvaise initialisation :

  • On adapte une nouvelle fois la méthode Build() pour être capable d'être dans l'état décrit :

  • Nos tests sont maintenant verts et on peut supprimer les constructeurs inutiles :

Encapsuler le Status

  • On commence par passer le set en private pour identifier les impacts

  • Nous devons retravailler le Builder pour qu'il supporte l'encapsulation

  • On adapte le Builder

Encapsuler les Events

  • En passant le setter des Events à private on observe que seuls les tests de consultation de status échouent

    • La consultation du status est déjà testé par ailleurs dans les tests d'Acceptance

    • Ces tests se recoupant, on peut dès lors supprimer les Tests Unitaires ConsulterStatus sans impact sur notre couverture et notre confiance

Encapsuler les collections

  • Nous avons grandement amélioré l'encapsulation mais nous pouvons aller plus loin

  • On va créer des backing fields pour ces 2 collections

  • Les "sécuriser" via l'utilisation d'une collection immutable

  • Ce changement va nous "forcer" à bouger une partie de la logique de la Factory dans le constructeur

  • On conserve les Guards dans la Factory et bouge l'instantiation dans le constructeur

Simplification des méthodes de Tir

  • La méthode Tirer ressemble à cela

  • On commence par inverser les if pour faire diminiuer la cyclomatic complexity

Invert if
  • On Save en même temps que l'Emit de l'Event

  • On nomme les guards pour rendre le code plus explicite

  • En appliquant la même technique sur TirerSurUneGalinette on se rend compte de la duplication :

  • On va créer une méthode private de tir permettant de mutualiser le code

Encapsulation du Chasseur

  • Depuis les méthodes de Tir on va appeler une méthode ATiré() plutôt que d'utiliser 1 setter

  • On fait de même pour les NbGalinettes

Un mot sur la duplication

En redescendant la logique dans le Domain et en refactorant ce dernier on a grandement amélioré la qualité du code :

No more duplication

Nouveau rapport SonarCloud disponible ici.

Reflect

  • Quel impact ce refactoring a eu sur les tests ?

  • Qu'est-ce qui est plus simple / compliqué maintenant ?

Last updated

Was this helpful?