L'enfer "zéro" et comment s'en sortir

Les valeurs nulles, lorsqu'elles sont utilisées sans réfléchir, peuvent rendre votre vie insupportable et vous ne pouvez même pas comprendre ce qui leur cause exactement une telle douleur. Laisse-moi t'expliquer.


Valeurs par défaut


Nous avons tous vu une méthode qui prend de nombreux arguments, mais plus de la moitié d'entre eux sont facultatifs. Le résultat est quelque chose comme ceci:

public function insertDiscount(   string $name,   int $amountInCents,   bool $isActive = true,   string $description = '',   int $productIdConstraint = null,   DateTimeImmutable $startDateConstraint = null,   DateTimeImmutable $endDateConstraint = null,   int $paymentMethodConstraint = null ): int 

Dans l'exemple ci-dessus, nous voulons créer une remise qui s'applique partout par défaut, mais elle peut être inactive lors de la création, s'appliquer uniquement à des produits spécifiques, n'agir qu'à certains moments ou s'appliquer lorsque l'utilisateur sélectionne un mode de paiement spécifique.

Si vous souhaitez créer une remise pour un certain mode de paiement, vous devrez appeler la méthode comme suit:

 insertDiscount('Discount name', 100, true, '', null, null, null, 5); 

Ce code fonctionnera, mais il est complètement incompréhensible pour la personne qui le lit. Il devient extrêmement difficile de l'analyser, nous ne pouvons donc pas facilement prendre en charge l'application.

Prenons cet exemple argument par argument.

Qu'est-ce qu'une remise valide?


Nous avons déjà découvert que la remise illimitée s'applique partout. Ainsi, une remise valide contient tout sauf les restrictions que nous pourrons ajouter plus tard. L'argument isActive a la valeur par défaut true. Par conséquent, la méthode peut être appelée comme suit:

 insertDiscount('Discount name', 100); 

Juste en lisant le code, je ne sais pas que la remise sera active immédiatement. Pour le savoir, je devrais vérifier si la signature de la méthode a des valeurs par défaut.

Imaginez maintenant que vous devez lire 200 lignes de code. Êtes-vous sûr de vouloir vérifier chaque signature de la méthode appelée pour des informations cachées? Je préfère simplement lire le code sans avoir à chercher quoi que ce soit.

Il en va de même pour l'argument responsable de la description. Par défaut, il s'agit d'une chaîne vide - cela peut provoquer de nombreux problèmes à l'endroit où vous vous attendez à voir une description. Par exemple, il peut être imprimé sur le chèque, mais comme il est vide, l'utilisateur voit simplement une ligne vide à côté de la ligne avec le montant. Le système ne devrait pas permettre que cela se produise.

Je réécrirais cette méthode comme ceci:

 public function insertDiscount(  string $name,  string $description,  int $amountInCents,  bool $isActive ): int 

J'ai complètement supprimé les restrictions car nous avons décidé de les ajouter plus tard en utilisant des méthodes distinctes. Étant donné que tous les paramètres sont désormais requis, ils peuvent être organisés dans n'importe quel ordre. J'ai placé la description juste après le nom, car le code se lit mieux lorsqu'ils sont proches.

 insertDiscount(  'Discount name',  'Discount description',  100,  Discount::STATUS_ACTIVE ); 

J'ai également utilisé des constantes pour le statut d'activité de remise. Maintenant, vous n'avez pas besoin de regarder la signature de la méthode pour savoir ce que signifie vraiment cet argument: il devient évident que nous créons une remise active. À l'avenir, nous pouvons encore améliorer cette méthode (spoiler: utilisation d'objets de valeur).

Ajout de contraintes


Vous pouvez maintenant ajouter diverses restrictions. Pour éviter zéro, zéro, zéro enfer, nous allons créer des méthodes distinctes.

 public function addProductConstraint(  Discount $discount,  int $productId ): Discount; public function addDateConstraint(  Discount $discount,  DateTimeImmutable $startDate,  DateTimeImmutable $endDate ): Discount; public function addPaymentMethodConstraint(  Discount $discount,  int $paymentMethod ): Discount; 

Ainsi, si nous voulons créer une nouvelle remise avec certaines restrictions, nous le ferons comme ceci:

 $discountId = insertDiscount(  'Discount name',  'Discount description',  100,  Discount::STATUS_ACTIVE ); addPaymentMethodConstraint(  $discountId,  PaymentMethod::CREDIT_CARD ); 

Comparez maintenant cela à l'appel d'origine. Vous verrez combien il est devenu plus pratique de lire.

Null dans les propriétés des objets


La résolution des zéros dans les propriétés des objets provoque également des problèmes. Je ne peux pas dire à quelle fréquence je vois de telles choses:

 $currencyCode = strtolower(  $record->currencyCode ); 

Buum! "Impossible de passer null à strtolower." Cela est arrivé parce que le développeur a oublié que currencyCode peut être nul. Étant donné que de nombreux développeurs n'utilisent toujours pas l'EDI ou n'y suppriment pas les avertissements, cela peut passer inaperçu pendant de nombreuses années. L'erreur continuera de rouler dans certains journaux non lus et les clients signaleront des problèmes périodiques qui ne sont apparemment pas liés à cela, donc personne ne se souciera de regarder cette ligne de code.

Nous pouvons, bien sûr, ajouter des chèques nuls partout où nous accédons à currencyCode. Mais alors nous finirons dans un autre type d'enfer:

 if ($record->currencyCode === null) {  throw new \RuntimeException('Currency code cannot be null'); } if ($record->amount === null) {  throw new \RuntimeException('Amount cannot be null'); } if ($record->amount > 0) {  throw new \RuntimeException('Amount must be a positive value'); } 

Mais, comme vous l'avez déjà compris, ce n'est pas la meilleure solution. En plus d'encombrer votre méthode, vous devez maintenant répéter ce test partout. Et chaque fois que vous ajoutez une autre propriété nulle, n'oubliez pas de refaire une telle vérification! Heureusement, il existe une solution simple: les objets de valeur.

Objets de valeur


Les objets de valeur sont des choses puissantes mais simples. Le problème que nous essayions de résoudre est qu'il est nécessaire de valider en permanence toutes nos propriétés. Mais nous le faisons parce que nous ne savons pas s'il est possible de faire confiance aux propriétés de l'objet, si elles sont valides. Et si on pouvait?

Pour faire confiance aux valeurs, ils ont besoin de deux attributs: ils doivent être validés et ne doivent pas avoir changé depuis la validation. Jetez un oeil à cette classe:

 final class Amount {  private $amountInCents;  private $currencyCode;  public function __construct(int $amountInCents, string $currencyCode): self  {    Assert::that($amountInCents)->greaterThan(0);    $this->amountInCents = $amountInCents;    $this->currencyCode = $currencyCode;  }  public function getAmountInCents(): int  {    return $this->amountInCents;  }  public function getCurrencyCode(): string  {    return $this->currencyCode;  } } 

J'utilise le package beberlei / assert. Il lève une exception chaque fois que la vérification échoue. C'est la même chose que l'exception pour null dans le code source, à moins que nous ayons déplacé la vérification vers ce constructeur.

Puisque nous utilisons des déclarations de type, nous garantissons que le type est également correct. Ainsi, nous ne pouvons pas passer int à strtolower. Si vous utilisez une ancienne version de PHP qui ne prend pas en charge les déclarations de type, vous pouvez utiliser ce package pour vérifier les types avec -> integer () et -> string ().

Après avoir créé l'objet, les valeurs ne peuvent pas être modifiées, car nous n'avons que des getters, mais pas de setters. C'est ce qu'on appelle l'immunité. L'ajout de final ne permet pas d'étendre cette classe pour ajouter des setters ou des méthodes magiques. Si vous voyez Amount $ amount dans les paramètres de la méthode, vous pouvez être sûr à 100% que toutes ses propriétés ont été validées et que l'objet est sûr à utiliser. Si les valeurs n'étaient pas valides, nous ne serions pas en mesure de créer un objet.

Maintenant, à l'aide d'objets de valeur, nous pouvons encore améliorer notre exemple:

 $discount = new Discount(  'Discount name',  'Discount description',  new Amount(100, 'CAD'),  Discount::STATUS_ACTIVE ) insertDiscount($discount); 

Veuillez noter que nous créons d'abord une remise, et à l'intérieur, nous utilisons le montant comme argument. Cela garantit que la méthode insertDiscount reçoit un objet de remise valide en plus de rendre ce bloc de code beaucoup plus facile à comprendre.

Zero Horror Story


Regardons un cas intéressant dans lequel null peut être nuisible dans une application. L'idée est d'extraire la collection de la base de données et de la filtrer.

 $collection = $this->findBy(['key' => 'value']); $result = $this->filter($collection, $someFilterMethod); if ($result === null) {   $result = $collection; } 

Si le résultat est nul, alors utilisez la collection d'origine comme résultat? Cela pose problème, car la méthode de filtrage renvoie null si elle n'a pas trouvé de valeurs appropriées. Ainsi, si tout est filtré, nous ignorerons le filtre et retournerons toutes les valeurs. Cela brise complètement la logique.

Pourquoi la collection originale est-elle utilisée en conséquence? Nous ne le saurons jamais. Je soupçonne que le développeur avait une certaine hypothèse sur ce que signifie null dans ce contexte, mais cela s'est avéré faux.

C'est le problème avec les valeurs nulles. Dans la plupart des cas, leur signification n'est pas claire et nous ne pouvons donc que deviner comment y répondre. Il est très facile de se tromper. L'exception, par contre, est très claire:

 try {  $result = $this->filter($collection, $someFilterMethod); } catch (CollectionCannotBeEmpty $e) {  // ... } 

Ce code est unique. Il est peu probable qu'un développeur l'interprète mal.

Vaut-il la peine?


Tout cela ressemble à un effort supplémentaire pour écrire du code qui fait la même chose. Oui, ça l'est. Mais en même temps, vous passerez beaucoup moins de temps à lire et à comprendre le code, donc les efforts seront assez récompensés. Chaque heure supplémentaire que je passe à écrire correctement du code me fait gagner des jours sans avoir à modifier le code ni à ajouter de nouvelles fonctionnalités. Considérez-le comme un investissement à haut rendement garanti.

La fin de ma tirade nulle est donc arrivée. J'espère que cela vous aidera à écrire du code plus compréhensible et plus facile à gérer.

Source: https://habr.com/ru/post/fr478760/


All Articles