Digitando: Tornando Estados Inválidos Inexpressíveis

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.


Criando ContactInfo


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.


ContactInfo Alterar


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.

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


All Articles