我向您介绍了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; }
此实现完全符合要求。 这三种情况都明确表示,而不允许第四种情况(没有任何地址)。
注意“电子邮件地址和邮政地址”的情况。 到目前为止,我只使用了一个元组。 在这种情况下,这就足够了。
现在让我们以如何使用此实现为例。 首先,创建一个新联系人:
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
,则必须处理三种可能的情况:
- 如果该联系人只有一个电子邮件地址,那么现在它具有两个地址,因此您需要使用构造函数
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
最后,如果逻辑是按类型表示的,那么对业务逻辑规则的任何更改都将破坏不考虑这些更改的代码,这通常是好的。
最后一点将在下一篇文章中介绍 。 尝试通过类型来表达业务逻辑规则,您可以对主题领域有深入的了解。