Taper: rendre les états invalides inexprimables

Je vous présente la traduction de l'article de Scott Wlaschin "Concevoir avec des types: rendre les états illégaux non représentables" .


Dans cet article, nous considérerons l'avantage clé de F # - la possibilité de "rendre des états incorrects inexprimables" en utilisant le système de type (la phrase a été empruntée à Yaron Minsky ).


Considérez le type Contact . À la suite de la refactorisation, il a grandement simplifié:


 type Contact = { Name: Name; EmailContactInfo: EmailContactInfo; PostalContactInfo: PostalContactInfo; } 

Supposons maintenant qu'il existe une règle commerciale simple: "Le contact doit contenir une adresse e-mail ou une adresse postale". Notre type respecte-t-il cette règle?


Non. Il découle de la règle qu'un contact peut contenir une adresse e-mail, mais ne pas avoir d'adresse postale, ou vice versa. Cependant, dans sa forme actuelle, le type nécessite que les deux champs soient remplis.


Il semble que la réponse soit évidente - rendez les adresses facultatives, par exemple, comme ceci:


 type Contact = { Name: PersonalName; EmailContactInfo: EmailContactInfo option; PostalContactInfo: PostalContactInfo option; } 

Mais maintenant, notre type permet trop. Dans cette implémentation, vous pouvez créer un contact sans adresse du tout, bien que la règle exige qu'au moins une adresse soit spécifiée.


Comment résoudre ce problème?


Comment rendre inexacts des états incorrects


Après avoir considéré la règle de la logique métier, nous pouvons conclure que trois cas sont possibles:


  • seule l'adresse e-mail est fournie;
  • seule l'adresse postale est indiquée;
  • Les adresses e-mail et postale sont fournies.

Dans une telle formulation, la solution devient évidente - faire une somme de types avec un constructeur pour chaque cas possible.


 type ContactInfo = | EmailOnly of EmailContactInfo | PostOnly of PostalContactInfo | EmailAndPost of EmailContactInfo * PostalContactInfo type Contact = { Name: Name; ContactInfo: ContactInfo; } 

Cette implémentation est entièrement conforme. Les trois cas sont exprimés explicitement, tandis que le quatrième cas (sans adresse) n'est pas autorisé.


Faites attention au cas «adresse e-mail et adresse postale». Jusqu'à présent, je viens d'utiliser un tuple. Dans ce cas, cela suffit.


Création de ContactInfo


Voyons maintenant comment utiliser cette implémentation comme exemple. Créez d'abord un nouveau contact:


 let contactFromEmail name emailStr = let emailOpt = EmailAddress.create emailStr //          match emailOpt with | Some email -> let emailContactInfo = {EmailAddress=email; IsEmailVerified=false} let contactInfo = EmailOnly emailContactInfo Some {Name=name; ContactInfo=contactInfo} | None -> None let name = {FirstName = "A"; MiddleInitial=None; LastName="Smith"} let contactOpt = contactFromEmail name "abc@example.com" 

Dans cet exemple, nous créons une fonction d'assistance simple contactFromEmail pour créer un nouveau contact en transmettant le nom et l'adresse e-mail. Cependant, l'adresse peut être incorrecte et la fonction doit gérer ces deux cas. La fonction ne peut pas créer un contact avec une adresse non valide, elle renvoie donc une valeur de type Contact option , pas Contact.


ContactInfo Changer


Si vous devez ajouter une adresse postale à un ContactInfo existant, vous devez gérer trois cas possibles:


  • si le contact n'avait qu'une adresse e-mail, il a maintenant les deux adresses, vous devez donc renvoyer le contact avec le constructeur EmailAndPost ;
  • si le contact n'avait qu'une adresse postale, vous devez renvoyer le contact avec le constructeur PostOnly , en remplaçant l'adresse postale par une nouvelle;
  • si le contact avait les deux adresses, vous devez renvoyer le contact avec le constructeur EmailAndPost , en remplaçant l'adresse postale par une nouvelle.

La fonction auxiliaire de mise à jour de l'adresse postale est la suivante. Notez le traitement explicite pour chaque cas.


 let updatePostalAddress contact newPostalAddress = let {Name=name; ContactInfo=contactInfo} = contact let newContactInfo = match contactInfo with | EmailOnly email -> EmailAndPost (email,newPostalAddress) | PostOnly _ -> //     PostOnly newPostalAddress | EmailAndPost (email,_) -> //     EmailAndPost (email,newPostalAddress) //    {Name=name; ContactInfo=newContactInfo} 

Et voici l'utilisation de ce code:


 let contact = contactOpt.Value //      option.Value  let newPostalAddress = let state = StateCode.create "CA" let zip = ZipCode.create "97210" { Address = { Address1= "123 Main"; Address2=""; City="Beverly Hills"; State=state.Value; //      option.Value  Zip=zip.Value; //      option.Value  }; IsAddressValid=false } let newContact = updatePostalAddress contact newPostalAddress 

AVERTISSEMENT: dans cet exemple, j'ai utilisé option.Value pour obtenir le contenu de l'option. C'est acceptable lorsque vous expérimentez dans une console interactive, mais c'est une terrible solution pour travailler le code! Vous devez toujours utiliser la correspondance de modèles et gérer les deux constructeurs d' option .


Pourquoi s'embêter avec ces types complexes?


À ce moment-là, vous pourriez décider que nous étions trop compliqués. Je vais répondre avec trois points.


Premièrement, la logique métier est complexe en soi. Il n'y a pas de moyen simple d'éviter cela. Si votre code est plus simple que la logique métier, vous ne gérez pas tous les cas comme il se doit.


Deuxièmement, si la logique est exprimée par des types, alors elle est auto-documentée. Vous pouvez consulter les constructeurs de type somme ci-dessous et comprendre immédiatement la règle métier. Vous n'avez pas à perdre de temps à analyser tout autre code.


 type ContactInfo = | EmailOnly of EmailContactInfo | PostOnly of PostalContactInfo | EmailAndPost of EmailContactInfo * PostalContactInfo 

Enfin, si la logique est exprimée par type, toute modification des règles de la logique métier rompra le code qui ne prend pas en compte ces modifications, ce qui est généralement bon.


Le dernier point est révélé dans le prochain article . En essayant d'exprimer les règles de la logique métier à travers les types, vous pouvez arriver à une compréhension approfondie du domaine.

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


All Articles