键入:使无效状态不可表达

我向您介绍了Scott Wlaschin的文章“使用类型进行设计:使非法国家无法代表”的翻译


在本文中,我们将考虑F#的主要优势-使用类型系统(使该短语从Yaron Minsky借用)“使不正确的状态不可表达”的能力。


考虑类型Contact 。 由于重构,他大大简化了:


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

现在,假设有一个简单的业务规则:“联系人必须包含电子邮件地址或邮政地址。” 我们的类型符合此规则吗?


不行 根据该规则,联系人可以包含电子邮件地址,但没有邮件地址,反之亦然。 但是,在当前形式下,该类型要求同时填写两个字段。


答案似乎很明显-将地址设为可选,例如:


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

但是现在我们的类型允许太多了。 在此实现中,尽管规则要求至少指定一个地址,但是您可以创建一个根本没有地址的联系人。


如何解决这个问题?


如何使错误状态无法表达


考虑了业务逻辑规则后,我们可以得出结论,三种情况是可能的:


  • 仅提供电子邮件地址;
  • 仅注明邮寄地址;
  • 提供电子邮件和邮政地址。

在这样的表述中,解决方案变得显而易见-针对每种可能的情况使用构造函数创建类型和。


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

此实现完全符合要求。 这三种情况都明确表示,而不允许第四种情况(没有任何地址)。


注意“电子邮件地址和邮政地址”的情况。 到目前为止,我只使用了一个元组。 在这种情况下,这就足够了。


创建ContactInfo


现在让我们以如何使用此实现为例。 首先,创建一个新联系人:


 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" 

在此示例中,我们创建了一个简单的帮助函数contactFromEmail以通过传递名称和电子邮件地址来创建新的联系人。 但是,地址可能不正确,并且该函数应处理这两种情况。 该函数无法创建具有无效地址的联系人,因此它返回的类型为Contact option ,而不是Contact。


ContactInfo更改


如果需要将邮件地址添加到现有的ContactInfo ,则必须处理三种可能的情况:


  • 如果该联系人只有一个电子邮件地址,那么现在它具有两个地址,因此您需要使用构造函数EmailAndPost返回该联系人;
  • 如果联系人只有一个邮寄地址,则必须使用PostOnly构造函数返回该联系人,并用新地址替换邮寄地址;
  • 如果联系人有两个地址,则需要使用构造函数EmailAndPost返回该联系人,并用新地址替换该邮寄地址。

更新邮件地址的辅助功能如下。 请注意每种情况的显式处理。


 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} 

这是此代码的用法:


 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 

警告:在此示例中,我使用option.Value获取option的内容。 在交互式控制台中进行实验时,这是可以接受的,但是对于工作代码来说,这是一个糟糕的解决方案! 您应该始终使用模式匹配并处理两个option构造函数。


为什么要打扰这些复杂的类型?


到这个时候,您可以决定我们太复杂了。 我将回答三点。


首先,业务逻辑本身很复杂。 没有简单的方法可以避免这种情况。 如果您的代码比业务逻辑更简单,那么您将无法按需处理所有情况。


其次,如果逻辑由类型表示,则它是自记录的。 您可以查看下面的sum-type构造函数,并立即了解业务规则。 您不必浪费时间分析任何其他代码。


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

最后,如果逻辑是按类型表示的,那么对业务逻辑规则的任何更改都将破坏不考虑这些更改的代码,这通常是好的。


最后一点将在下一篇文章中介绍 。 尝试通过类型来表达业务逻辑规则,您可以对主题领域有深入的了解。

Source: https://habr.com/ru/post/zh-CN424895/


All Articles