Apresento a você a tradução do artigo de Scott Wlaschin "Projetando com tipos: Tornando estados ilegais irrepresentáveis" .
Neste artigo, consideraremos a principal vantagem do F # - a capacidade de "tornar estados incorretos inexprimíveis" usando o sistema de tipos (a frase foi emprestada por Yaron Minsky ).
Considere o tipo Contact
. Como resultado da refatoração, ele simplificou bastante:
type Contact = { Name: Name; EmailContactInfo: EmailContactInfo; PostalContactInfo: PostalContactInfo; }
Agora, suponha que exista uma regra comercial simples: "O contato deve conter um endereço de email ou endereço postal". Nosso tipo está em conformidade com esta regra?
Não. Segue-se da regra que um contato pode conter um endereço de email, mas não ter um endereço para correspondência ou vice-versa. No entanto, em seu formulário atual, o tipo requer que ambos os campos sejam preenchidos.
Parece que a resposta é óbvia - torne os endereços opcionais, por exemplo, assim:
type Contact = { Name: PersonalName; EmailContactInfo: EmailContactInfo option; PostalContactInfo: PostalContactInfo option; }
Mas agora nosso tipo permite demais. Nesta implementação, você pode criar um contato sem endereço, embora a regra exija que pelo menos um endereço seja especificado.
Como resolver este problema?
Como tornar estados incorretos inexprimíveis
Tendo considerado a regra da lógica de negócios, podemos concluir que três casos são possíveis:
- somente endereço de email é fornecido;
- apenas o endereço para correspondência é indicado;
- São fornecidos endereços de email e postais.
Em tal formulação, a solução se torna óbvia - fazer uma soma de tipo com um construtor para cada caso possível.
type ContactInfo = | EmailOnly of EmailContactInfo | PostOnly of PostalContactInfo | EmailAndPost of EmailContactInfo * PostalContactInfo type Contact = { Name: Name; ContactInfo: ContactInfo; }
Esta implementação é totalmente compatível. Todos os três casos são expressos explicitamente, enquanto o quarto caso (sem nenhum endereço) não é permitido.
Preste atenção ao caso de "endereço de email e endereço postal". Até agora, eu apenas usei uma tupla. Nesse caso, isso é suficiente.
Agora vamos ver como usar essa implementação como um exemplo. Primeiro, crie um novo contato:
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"
Neste exemplo, criamos uma função auxiliar simples contactFromEmail
para criar um novo contato, passando o nome e o endereço de email. No entanto, o endereço pode estar incorreto e a função deve lidar com esses dois casos. A função não pode criar um contato com um endereço inválido; portanto, ele retorna um valor do tipo Contact option
, não contato.
Se você precisar adicionar um endereço para correspondência a um ContactInfo
existente, precisará lidar com três casos possíveis:
- se o contato tiver apenas um endereço de email, agora ele possui os dois endereços; portanto, você deve retornar o contato com o construtor
EmailAndPost
; - se o contato tiver apenas um endereço para correspondência, você deverá retornar o contato com o construtor
PostOnly
, substituindo o endereço por um novo; - se o contato tiver os dois endereços, você precisará retornar o contato com o construtor
EmailAndPost
, substituindo o endereço de correspondência por um novo.
A função auxiliar para atualizar o endereço para correspondência é a seguinte. Observe o processamento 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}
E aqui está o uso deste 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
AVISO: Neste exemplo, usei option.Value
para obter o conteúdo da opção. Isso é aceitável quando você está experimentando em um console interativo, mas é uma solução terrível para trabalhar com código! Você sempre deve usar a correspondência de padrões e manipular os dois construtores de option
.
Por que se preocupar com esses tipos complexos?
A essa altura, você poderia decidir que éramos todos muito complicados. Eu responderei com três pontos.
Primeiro, a lógica de negócios é complexa por si só. Não há maneira fácil de evitar isso. Se o seu código for mais simples que a lógica comercial, você não lida com todos os casos como deveria.
Em segundo lugar, se a lógica é expressa por tipos, ela é auto-documentada. Você pode olhar para os construtores do tipo soma abaixo e entender imediatamente a regra de negócios. Você não precisa perder tempo analisando qualquer outro código.
type ContactInfo = | EmailOnly of EmailContactInfo | PostOnly of PostalContactInfo | EmailAndPost of EmailContactInfo * PostalContactInfo
Finalmente, se a lógica for expressa por tipo, qualquer alteração nas regras da lógica de negócios quebrará o código que não leva em conta essas alterações, e isso geralmente é bom.
O último ponto é revelado no próximo artigo . Tentando expressar as regras da lógica de negócios por meio de tipos, você pode obter um entendimento aprofundado da área de assunto.