Ich präsentiere Ihnen die Übersetzung von Scott Wlaschins Artikel "Entwerfen mit Typen: Illegale Staaten nicht darstellbar machen" .
In diesem Artikel werden wir den Hauptvorteil von F # betrachten - die Fähigkeit, "falsche Zustände unaussprechlich zu machen", indem wir das Typsystem verwenden (die Phrase wurde von Yaron Minsky entlehnt).
Betrachten Sie den Typ Contact
. Infolge des Refactorings vereinfachte er stark:
type Contact = { Name: Name; EmailContactInfo: EmailContactInfo; PostalContactInfo: PostalContactInfo; }
Angenommen, es gibt eine einfache Geschäftsregel: "Der Kontakt muss eine E-Mail-Adresse oder eine Postanschrift enthalten." Entspricht unser Typ dieser Regel?
Nein. Aus der Regel folgt, dass ein Kontakt eine E-Mail-Adresse enthalten kann, aber keine Postanschrift hat oder umgekehrt. In der aktuellen Form mĂĽssen fĂĽr den Typ jedoch beide Felder ausgefĂĽllt werden.
Es scheint, dass die Antwort offensichtlich ist - machen Sie die Adressen beispielsweise wie folgt optional:
type Contact = { Name: PersonalName; EmailContactInfo: EmailContactInfo option; PostalContactInfo: PostalContactInfo option; }
Aber jetzt erlaubt unser Typ zu viel. In dieser Implementierung können Sie einen Kontakt ohne Adresse erstellen, obwohl nach der Regel mindestens eine Adresse angegeben werden muss.
Wie kann man dieses Problem lösen?
Wie man falsche Zustände unaussprechlich macht
Nachdem wir die Regel der Geschäftslogik berücksichtigt haben, können wir schließen, dass drei Fälle möglich sind:
- Es wird nur die E-Mail-Adresse angegeben.
- es wird nur die Postanschrift angegeben;
- Es werden sowohl E-Mail- als auch Postanschriften angegeben.
In einer solchen Formulierung wird die Lösung offensichtlich - für jeden möglichen Fall eine Typensumme mit einem Konstruktor zu erstellen.
type ContactInfo = | EmailOnly of EmailContactInfo | PostOnly of PostalContactInfo | EmailAndPost of EmailContactInfo * PostalContactInfo type Contact = { Name: Name; ContactInfo: ContactInfo; }
Diese Implementierung ist vollständig konform. Alle drei Fälle werden explizit ausgedrückt, während der vierte Fall (ohne Adresse) nicht zulässig ist.
Achten Sie auf den Fall "E-Mail-Adresse und Postanschrift". Bisher habe ich nur ein Tupel verwendet. In diesem Fall ist dies ausreichend.
Lassen Sie uns nun sehen, wie diese Implementierung als Beispiel verwendet wird. Erstellen Sie zunächst einen neuen Kontakt:
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"
In diesem Beispiel erstellen wir eine einfache contactFromEmail
, um einen neuen Kontakt zu erstellen, indem wir den Namen und die E-Mail-Adresse übergeben. Die Adresse ist jedoch möglicherweise falsch, und die Funktion sollte beide Fälle behandeln. Die Funktion kann keinen Kontakt mit einer ungültigen Adresse erstellen, daher wird ein Wert vom Typ Kontaktoption zurückgegeben, nicht Kontakt.
Wenn Sie einer vorhandenen ContactInfo
eine Postanschrift hinzufĂĽgen ContactInfo
, müssen Sie drei mögliche Fälle behandeln:
- Wenn der Kontakt nur eine E-Mail-Adresse hatte, hat er jetzt beide Adressen, sodass Sie den Kontakt mit dem Konstruktor
EmailAndPost
. - Wenn der Kontakt nur eine Postanschrift hatte, mĂĽssen Sie den Kontakt an den
PostOnly
Konstruktor zurĂĽckgeben und die Postanschrift durch eine neue ersetzen. - Wenn der Kontakt beide Adressen hatte, mĂĽssen Sie den Kontakt mit dem Konstruktor
EmailAndPost
und die Postanschrift durch eine neue ersetzen.
Die Hilfsfunktion zum Aktualisieren der Postanschrift lautet wie folgt. Beachten Sie die explizite Verarbeitung fĂĽr jeden Fall.
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}
Und hier ist die Verwendung dieses Codes:
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
WARNUNG: In diesem Beispiel habe ich option.Value
, um den Inhalt der Option option.Value
. Dies ist akzeptabel, wenn Sie in einer interaktiven Konsole experimentieren, aber es ist eine schreckliche Lösung für die Arbeit mit Code! Sie sollten immer den Mustervergleich verwenden und beide option
.
Warum sich mit diesen komplexen Typen beschäftigen?
Zu diesem Zeitpunkt konnten Sie feststellen, dass wir alle zu kompliziert waren. Ich werde mit drei Punkten antworten.
Erstens ist die Geschäftslogik an sich komplex. Es gibt keine einfache Möglichkeit, dies zu vermeiden. Wenn Ihr Code einfacher als die Geschäftslogik ist, behandeln Sie nicht alle Fälle wie gewünscht.
Zweitens, wenn Logik durch Typen ausgedrückt wird, ist sie selbstdokumentierend. Sie können sich die Summenkonstruktoren unten ansehen und die Geschäftsregel sofort verstehen. Sie müssen keine Zeit damit verschwenden, anderen Code zu analysieren.
type ContactInfo = | EmailOnly of EmailContactInfo | PostOnly of PostalContactInfo | EmailAndPost of EmailContactInfo * PostalContactInfo
Wenn die Logik nach Typ ausgedrückt wird, brechen Änderungen an den Regeln der Geschäftslogik den Code, der diese Änderungen nicht berücksichtigt, und dies ist normalerweise gut.
Der letzte Punkt wird im nächsten Artikel offenbart. Wenn Sie versuchen, die Regeln der Geschäftslogik durch Typen auszudrücken, können Sie ein tiefgreifendes Verständnis des Themenbereichs erlangen.