我正在创建一种“信封”来跟踪通过应用程序中RabbitMQ发送的内容(命令和事件),如下所示:
type Envelope<'T> = {
Id: Guid
CausationId: Guid
CorrelationId: Guid
Payload: 'T
Timestamp: DateTimeOffset
}
'T
通常是以下形式的命令或事件:
type Commands =
| DoA of Stuff
| DoB of Stuff
| DoC of Stuff
type Events =
| AHappened of Stuff
| BHappened of Stuff
| CHappened of Stuff
注意:我故意使用相对通用的名称
但是,我也可以使用特定类型:
type CausationId = CausationId of Guid
type CorrelationId = CorrelationId of Guid
type CommandId = CommandId of Guid
type EventId = EventId of Guid
type Timestamp = Timestamp of DateTimeOffset
type DataToDoA = DataToDoA of Stuff
type DataToDoB = DataToDoB of Stuff
type DataToDoC = DataToDoC of Stuff
type Commands =
| DoA of DataToDoA
| DoB of DataToDoB
| DoC of DataToDoC
type DataLeftByA = DataLeftByA of Stuff
type DataLeftByB = DataLeftByB of Stuff
type DataLeftByC = DataLeftByC of Stuff
type Events =
| AHappened of DataLeftByA
| BHappened of DataLeftByB
| CHappened of DataLeftByC
这会导致:
type CommandEnvelope<`T> = {
Id: CommandId
CausationId: CausationId
CorrelationId: CorrelationId
Payload: `T
Timestamp: Timestamp
}
type EventEnvelope<`T> = {
Id: EventId
CausationId: CausationId
CorrelationId: CorrelationId
Payload: `T
Timestamp: Timestamp
}
是否有经验法则来决定何时使用这些特定类型/别名?
答案 0 :(得分:2)
我认为这没有经验法则,但是一种系统的思考方式是考虑是否需要将那些额外类型表示的值自己传递给代码。如果否,那么您不需要那些额外的类型。如果是,那么它们可能会有用。
假设您只有Stuff
并且:
type DataLeftByA = DataLeftByA of Stuff
type DataLeftByB = DataLeftByB of Stuff
type Events =
| AHappened of DataLeftByA
| BHappened of DataLeftByB
这可以让您做的一件事就是编写一个函数:
let processDataA (DataLeftByA stuff) = (...)
此函数需要DataLeftByA
包含一些Stuff
。但是,该类型清楚地表明该功能仅应用于由A
引起的事件。以下将是类型错误:
let handleEvent = function
| AHappened adata -> processDataA adata
| BHappened bdata -> processDataA bdata // Type error here!
如果您将Events
定义为包含相同Stuff
的事件:
type Events =
| AHappened of Stuff
| BHappened of Stuff
然后,事件所携带的数据是相同的,但是您失去了定义诸如processDataA
之类的功能的能力,因为事件A
所携带的数据没有单独的类型。您可以只定义processStuff
,但是可以在A
和B
情况下调用它。
我认为这是在两个版本之间产生真正实际差异的唯一内容。因此,经验法则是-您是否需要定义一个像processDataA
这样的函数?
答案 1 :(得分:1)
Scott Wlaschin of F# for Fun and Profit talks a bit about this in his "Designing with Types" series. In the post on Single Case Union Types, he explains that one of the main reasons for using single-case union-types is when you want to add some validation logic. This allows you to refine your type a bit more.
The example he gives is email. You could have type Person = { ... Email: string ... }
but this does not provide any guarantee that the string is an email address. Making a newtype for it type EmailAddress = Email of string
you can then you can only create email addresses from functions which validate it first. This can be a good sanity check and helps make sure you don't assign email addresses to things which aren't addresses/links.
TL;DR Single-case union types are useful for whenever you want to add further semantics/validation to your code.