Le presento la traducción del artículo de Scott Wlaschin "Diseñando con tipos: haciendo que los estados ilegales sean irrepresentables" .
En este artículo, consideraremos la ventaja clave de F #: la capacidad de "hacer que los estados incorrectos sean inexpresables" utilizando el sistema de tipos (la frase fue tomada de Yaron Minsky ).
Considere el tipo de Contact
. Como resultado de la refactorización, simplificó enormemente:
type Contact = { Name: Name; EmailContactInfo: EmailContactInfo; PostalContactInfo: PostalContactInfo; }
Ahora suponga que hay una regla comercial simple: "El contacto debe contener una dirección de correo electrónico o una dirección postal". ¿Nuestro tipo cumple con esta regla?
No De la regla se deduce que un contacto puede contener una dirección de correo electrónico, pero no tener una dirección de correo, o viceversa. Sin embargo, en su forma actual, el tipo requiere que se llenen ambos campos.
Parece que la respuesta es obvia: haga que las direcciones sean opcionales, por ejemplo, así:
type Contact = { Name: PersonalName; EmailContactInfo: EmailContactInfo option; PostalContactInfo: PostalContactInfo option; }
Pero ahora nuestro tipo permite demasiado. En esta implementación, puede crear un contacto sin ninguna dirección, aunque la regla requiere que se especifique al menos una dirección.
¿Cómo resolver este problema?
Cómo hacer que los estados incorrectos sean inexpresables
Habiendo considerado la regla de la lógica empresarial, podemos concluir que son posibles tres casos:
- solo se proporciona una dirección de correo electrónico;
- solo se indica la dirección postal;
- Se proporcionan direcciones de correo electrónico y postales.
En tal formulación, la solución se vuelve obvia: hacer una suma de tipos con un constructor para cada caso posible.
type ContactInfo = | EmailOnly of EmailContactInfo | PostOnly of PostalContactInfo | EmailAndPost of EmailContactInfo * PostalContactInfo type Contact = { Name: Name; ContactInfo: ContactInfo; }
Esta implementación es totalmente compatible. Los tres casos se expresan explícitamente, mientras que el cuarto caso (sin ninguna dirección) no está permitido.
Preste atención al caso de "dirección de correo electrónico y dirección postal". Hasta ahora solo usé una tupla. En este caso, esto es suficiente.
Ahora veamos cómo usar esta implementación como ejemplo. Primero, cree un nuevo contacto:
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"
En este ejemplo, creamos una función auxiliar simple contactFromEmail
para crear un nuevo contacto pasando el nombre y la dirección de correo electrónico. Sin embargo, la dirección puede ser incorrecta y la función debe manejar ambos casos. La función no puede crear un contacto con una dirección no válida, por lo que devuelve un valor de tipo Contact option
, no Contacto.
Si necesita agregar una dirección de correo a un ContactInfo
existente, debe manejar tres casos posibles:
- si el contacto tenía solo una dirección de correo electrónico, ahora tiene ambas direcciones, por lo que debe devolver el contacto con el constructor
EmailAndPost
; - si el contacto solo tenía una dirección postal, debe devolver el contacto con el constructor
PostOnly
, reemplazando la dirección postal por una nueva; - Si el contacto tenía ambas direcciones, debe devolver el contacto con el constructor
EmailAndPost
, reemplazando la dirección de correo por una nueva.
La función auxiliar para actualizar la dirección de correo es la siguiente. Tenga en cuenta el procesamiento explícito para cada caso.
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}
Y aquí está el uso de este código:
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
ADVERTENCIA: en este ejemplo, utilicé option.Value
para obtener el contenido de la opción. Esto es aceptable cuando está experimentando en una consola interactiva, ¡pero es una solución terrible para el código de trabajo! Siempre debe usar la coincidencia de patrones y manejar ambos constructores de option
.
¿Por qué molestarse con estos tipos complejos?
En este momento, podrías decidir que todos somos demasiado complicados. Contestaré con tres puntos.
Primero, la lógica de negocios es compleja en sí misma. No hay una manera fácil de evitar esto. Si su código es más simple que la lógica de negocios, no maneja todos los casos como debería.
En segundo lugar, si la lógica se expresa por tipos, entonces es autodocumentada. Puede ver los constructores de tipo suma a continuación e inmediatamente comprender la regla de negocios. No tiene que perder el tiempo analizando ningún otro código.
type ContactInfo = | EmailOnly of EmailContactInfo | PostOnly of PostalContactInfo | EmailAndPost of EmailContactInfo * PostalContactInfo
Finalmente, si la lógica se expresa por tipo, cualquier cambio en las reglas de la lógica empresarial romperá el código que no tiene en cuenta estos cambios, y esto generalmente es bueno.
El último punto se revela en el próximo artículo . Al tratar de expresar las reglas de la lógica empresarial a través de los tipos, puede llegar a una comprensión profunda del área temática.