我不禁要问,在大型系统中使用歧视联盟是否违反了开放/关闭原则.
我理解开放/关闭原则是面向对象的而不是功能性的.但是,我有理由相信存在相同的代码气味.
我经常避免使用switch语句,因为我经常被迫处理最初没有考虑的案例.因此,我发现自己必须使用新案例和一些相对行为来更新每个引用.
因此,我仍然认为,Discriminated Unions具有与switch-statements相同的代码味道.
我的想法准确吗?
为什么switch语句不受欢迎,但被歧视的联盟被接受了?
使用Discriminated Unions时是否会遇到与我们在代码库演变或离题时执行switch语句相同的维护问题?
对象和受歧视的联盟具有彼此双重的限制:
使用接口时,很容易添加实现接口的新类而不影响其他实现,但很难添加新方法(即,如果添加新方法,则需要将方法的实现添加到实现接口的每个类).
在设计DU类型时,很容易使用该类型添加新方法而不影响其他方法,但很难添加新案例(即,如果添加新案例,则需要更新每个现有方法来处理它).
因此,DUs绝对不适合对每个问题进行建模; 但传统的OO设计也不是.通常,你知道你需要在哪个"方向"进行未来的修改,所以它很容易选择(例如列表肯定是空的或者有头和尾,所以通过DU建模它们是有意义的).
有时您希望能够在两个方向上扩展(添加新的"种类"对象并添加新的"操作") - 这与表达式问题有关,并且在经典OO编程中没有特别干净的解决方案或经典的FP编程(虽然有点巴洛克式的解决方案是可能的,例如见的Vesa Karvonen的评论在这里,我已经向音译F#这里).
可以看到DU比switch语句更有利的一个原因是F#编译器对穷举和冗余检查的支持可能比C#编译器检查switch语句更彻底(例如,如果我有match x with | A -> 'a' | B -> 'b'
,我添加一个新的DU情况C
然后我会得到一个警告/错误,但是当enum
在C#中使用时我需要有一个default
案例,所以编译时检查不能那么强.
在我看来,开放/封闭原则有点模糊 - "开放扩展"究竟意味着什么?
这是指用新数据扩展,还是用新行为扩展,或两者兼而有之?
以下是Betrand Meyer的引用(取自维基百科):
类是关闭的,因为它可以被编译,存储在库中,基线化并由客户端类使用.但它也是开放的,因为任何新类都可以将它用作父级,添加新功能.定义后代类时,无需更改原始内容或干扰其客户端.
以下是罗伯特·马丁的文章引用:
开放封闭原则以非常直接的方式对此进行攻击.它说你应该设计永不改变的模块.当需求发生变化时,您可以通过添加新代码来扩展此类模块的行为,而不是通过更改已经运行的旧代码.
我从这些引言中剔除的是强调永不打破依赖于你的客户.
在面向对象的范例(基于行为)中,我将其解释为使用接口(或抽象基类)的建议.然后,如果需求发生更改,您可以创建现有接口的新实现,或者,如果需要新行为,则创建一个扩展原始接口的新接口.(顺便说一句,switch语句不是OO - 你应该使用多态!)
在功能范例中,从设计的角度来看,接口的等价物是一个功能.就像在OO设计中将接口传递给对象一样,您可以将函数作为参数传递给FP设计中的另一个函数.更重要的是,在FP中,每个功能签名都自动成为"界面"!只要函数签名不变,函数的实现就可以在以后更改.
如果确实需要新行为,只需定义一个新函数 - 旧函数的现有客户端不会受到影响,而需要修改需要此新功能的客户端才能接受新参数.
现在,在F#中更改DU的需求的特定情况下,您可以通过两种方式扩展它而不会影响客户端.
使用组合从旧的数据类型构建新的数据类型,或
从客户端隐藏案例并使用活动模式.
假设你有一个像这样的简单DU:
type NumberCategory = | IsBig of int | IsSmall of int
并且您想要添加一个新案例IsMedium
.
在合成方法中,您将创建一个新类型而不触及旧类型,例如:
type NumberCategoryV2 = | IsBigOrSmall of NumberCategory | IsMedium of int
对于只需要原始NumberCategory
组件的客户端,您可以将新类型转换为旧类型,如下所示:
// convert from NumberCategoryV2 to NumberCategory let toOriginal (catV2:NumberCategoryV2) = match catV2 with | IsBigOrSmall original -> original | IsMedium i -> IsSmall i
您可以将此视为一种明确的向上转换:)
或者,您可以隐藏案例并仅显示活动模式:
type NumberCategory = private // now private! | IsBig of int | IsSmall of int let createNumberCategory i = if i > 100 then IsBig i else IsSmall i // active pattern used to extract data since type is private let (|IsBig|IsSmall|) numberCat = match numberCat with | IsBig i -> IsBig i | IsSmall i -> IsSmall i
稍后,当类型更改时,您可以更改活动模式以保持兼容:
type NumberCategory = private | IsBig of int | IsSmall of int | IsMedium of int // new case added let createNumberCategory i = if i > 100 then IsBig i elif i > 10 then IsMedium i else IsSmall i // active pattern used to extract data since type is private let (|IsBig|IsSmall|) numberCat = match numberCat with | IsBig i -> IsBig i | IsSmall i -> IsSmall i | IsMedium i -> IsSmall i // compatible with old definition
哪种方法最好?
好吧,对于我完全控制的代码,我也不会使用 - 我只是对DU进行更改并修复编译器错误!
对于作为API公开给我无法控制的客户端的代码,我会使用主动模式方法.