我正在尝试在F#中练习域驱动设计,并偶然发现了以下问题:
为什么我在使用元组时使用记录似乎需要更少的语法,并且在模式匹配和整体使用场景方面看起来更强大?
例如,如果我改为使用元组,我觉得好像我不再需要创建一个记录类型来实现一个有区别的联合。
type Name = { First:string
Middle:string option
Last:string }
type Duration = { Hours:int
Minutes:int
Seconds:int }
type Module =
| Author of Name
| Title of string
| Duration of Duration
let tryCreateName (first, middle, last) =
{ First=first; Middle=Some middle; Last=last }
let tryCreateDuration (hours, minutes, seconds) =
{ Hours=hours; Minutes=minutes;Seconds=seconds }
let name = tryCreateName ("Scott", "K", "Nimrod")
let hours = 1
let minutes = 30
let seconds = 15
let duration = tryCreateDuration (hours, minutes, seconds)
我的想法准确吗?
在大多数情况下,元组是否需要记录?
答案 0 :(得分:14)
对于域建模,我建议使用带有命名元素的类型;也就是说,记录,歧视联盟,也许是偶尔的类或界面。
在结构上,记录和元组是相似的;在代数数据类型的说法中,它们都是产品类型。
不同之处在于,对于元组,值的顺序很重要,每个元素的作用都是隐含的。
> (2016, 1, 2) = (2016, 1, 2);;
val it : bool = true
> (2016, 1, 2) = (2016, 2, 1);;
val it : bool = false
在上面的例子中,您可能会猜测这些元组建模日期,但究竟是哪些?这是2016年1月的第二天吗?或者它是2016年2月的第一个?
另一方面,对于记录,元素的顺序并不重要,因为您绑定它们并按名称访问它们:
> type Date = { Year : int; Month : int; Day : int };;
type Date =
{Year: int;
Month: int;
Day: int;}
> { Year = 2016; Month = 1; Day = 2 } = { Year = 2016; Day = 2; Month = 1 };;
val it : bool = true
当您想要提取成分值时,它也会更清晰。您可以轻松地从记录值中获取年份:
> let d = { Year = 2016; Month = 1; Day = 2 };;
val d : Date = {Year = 2016;
Month = 1;
Day = 2;}
> d.Year;;
val it : int = 2016
从元组中提取价值要困难得多:
> let d = (2016, 1, 2);;
val d : int * int * int = (2016, 1, 2)
> let (y, _, _) = d;;
val y : int = 2016
当然,对于对,您可以使用内置函数fst
和snd
来访问元素,但对于具有三个或更多元素的元组,您无法轻松获取除非你模式匹配,否则这些值。
即使您定义自定义函数,每个元素的角色仍然由其序数隐式定义。如果它们属于同一类型,那么很容易让错误的顺序错误。
因此,对于域建模,我总是喜欢显式类型,以便明确发生了什么。
那些元组永远不合适吗?
元组在其他环境中很有用。当您需要使用 ad hoc类型来编写函数时,它们比记录更合适。
作为一个例子,考虑Seq.zip
,它使您能够组合两个序列:
let alphalues = Seq.zip ['A'..'Z'] (Seq.initInfinite ((+) 1)) |> Map.ofSeq;;
val alphalues : Map<char,int> =
map
[('A', 1); ('B', 2); ('C', 3); ('D', 4); ('E', 5); ('F', 6); ('G', 7);
('H', 8); ('I', 9); ...]
> alphalues |> Map.find 'B';;
val it : int = 2
正如您在此示例中所看到的,元组只是向实际目标迈出的一步,这是一个按字母顺序排列的值的映射。如果我们每次想要在表达式内组合值时必须定义记录类型,那就太尴尬了。元组非常适合这项任务。
答案 1 :(得分:1)
您可能对optional parameters感兴趣,只允许在类型上使用。为避免更改参数的顺序,我也将姓氏设为可选的(因此,它也可以更好地模拟您的问题域,例如遇到Mononyms时)。
为了表示两个时间点之间的差异,框架中已有一个库函数System.TimeSpan
。
因此,您的示例可以写成
type Name = {
FirstName : string
MiddleName : string option
LastName : string option } with
static member Create(firstName, ?middleName, ?lastName) =
{ FirstName = firstName
MiddleName = middleName
LastName = lastName }
type System.TimeSpan with
static member CreateDuration(hours, ?minutes, ?seconds) =
System.TimeSpan(
hours,
defaultArg minutes 0,
defaultArg seconds 0 )
let name = Name.Create("Scott", "K", "Nimrod")
let duration = System.TimeSpan.CreateDuration(1, 30, 15)
这个想法是提供extension methods,它采用不同长度的元组参数,并返回一个复杂的数据结构,例如记录或结构。