F#奇妙游(25):ADT和领域设计
前言
采用ADT来对领域进行设计,是一种很好的实践。在这种实践中,我们可以把领域中的数据抽象成ADT,把领域中的操作抽象成函数,然后利用ADT的类型系统来进行类型检查,从而保证领域中的数据和操作的正确性。
设计目标
首先,我们的设计目标如下:
- 各领域专家架和程序开发都能够理解的设计
- 能够利用ADT的类型系统来进行类型检查
- 不需要与具体的编程语言乃至编程范式绑定
设计原则
那么我们需要遵循大概以下的原则:
- 描述ADT的术语应该尽量与领域中的术语一致
- 描述ADT的数据结构应该尽量与领域中的数据结构一致
- 描述ADT的操作应该尽量与领域中的操作一致
- ADT的分析应该专注于组合而不是继承
- ADT的各个组合部分都应该都领域含义,而非编程语言的实现细节
- 编程实现的细节(例如基本数据类型、数据结构和算法的实现)在设计阶段应该被忽略
软件开发流程
从本质上来看,所有的设计都应该是迭代的。不过我们还是可以首先给出一个线性的流程,然后再给出检查点,并定义迭代的流程。
首先是整个软件系统开发的流程,如果我们非常丧心病狂,并且非常注重完备性,务必要把所有的步骤都走一遍,那么整个流程如下:
如果我们把编程实现、测试和部署抽象为系统实现,那么这个图会稍微好看一点。
当我们暂时把系统实现扔掉,那么整个流程就是:
领域分析
领域驱动设计中,领域分析的核心内容系统的事件分析,一般会采用事件风暴(Event Storming)的方法来进行。在这个过程中,我们会把领域中的事件抽象为事件(Event),把事件中的数据抽象为值对象(Value Object),把事件中的操作抽象为实体(Entity)。
并进一步考虑事件的触发条件或者触发事件,以及事件的结果,这些都是领域中的事件,但是不是所有的事件都是我们需要关注的,我们只需要关注那些对我们的系统有意义的事件。这个过程最重要的原则就是,到底到边。
一直要事件的分析推到系统的边界上,或者是某个Actor的动作触发了事件,或者某个状态触发了事件,而事件的发生又会改变系统的状态,或者触发另外的事件。这其中比较重要的就是对现存系统的分析,以及对现存系统的边界的分析。
当通过事件风暴把系统的事件分析完毕之后,需要的就是进行事件的分类、排序,分析触发事件的命令,把一系列事件组合起来,形成一个个有逻辑的序列,在这个基础上,可进行子领域的划分,以及子领域之间的关系的分析。每个子领域应该对应特定的领域专家,或者是特定的团队,这样可以保证领域专家的专业性,也可以保证团队的独立性。
事件的内涵外延、相互关系定义取得领域专家的一致意见后,需要领域专家进一步分析事件的输入和输出,事件的输入和输出就是事件的数据,这些数据可以是值对象,也可以是实体。这个过程中,需要领域专家对事件的输入和输出进行分析,然后把这些数据抽象为值对象或者实体。
所以领域分析大概内容就是事件分析、命令与业务流程、子领域与上下文等。
进一步ADT分析与设计,就可以形式化描述领域共享模型。
领域流程ADT
根据前面的分析,领域的模型由命令、事件和业务流程构成,并形成子领域、有界上下文,最终能够捕捉领域中的值的流动和业务信息。
在这个领域模型的核心,是信息传递和传唤的过程。这个过程可能称为流程、操作这类动词。在F#这类函数式编程中,则用函数来表达。
这里三个元素:合法的输入集合、函数、合法的输出集合,都可以抽象为一个抽象数据类型(ADT),而ADT的组合数,就对应着输入输出的合法性以及函数对应关系。
这个样子,领域流程或者操作的编程对应就非常清楚,同样是一个ADT,可以表达为 A D T 1 ↦ A D T 2 ADT_1 \mapsto ADT_2 ADT1↦ADT2 。
所以领域的流程、流程的输入输出对应就是进行ADT设计,而对ADT的组合数则可用于实现关于合法的相关约束,这样才能做到非法状态不可表示。
ADT设计
ADT设计的核心就是对领域中的数据和操作进行抽象,这个过程中,我们需要把领域中的数据抽象为值对象或者实体,并用ADT来描述;并把领域中的操作抽象为ADT映射(同样是一个ADT)。
在ADT设计中,F#能提供什么支持呢?根据前面ADT的分析和学习,我们知道,ADT的两种基本形式就是Sum和Product。
Sum类型是OR的关系,Product类型是AND的关系。对领域中的值、表达式进行描述时,如果采用OR和AND来表示,大概会是:
data InputData = DataID
AND DataField1
AND DataFiled2
AND DataFiled3
data DataID = DataName
OR DataSerialNumber
上面这样的描述,可以将值的类型表达为领域专家所能够理解的形式,输入数据包括标识和数个字段;输入数据的标识可能是字符串也可能是序列号。
从前面ADT的分析和介绍看,组合数分析是非常总要的。因此,对于ADT中所包含的类型,需要进一步根据领域情况分析。例如
data DataName of string
length in 12..255
ascii a..z, A..Z, 0..9
first 8 chars: valid date string
ex. 20230830xxxx
data DataSerialNumber of int
12 digits
first 8 digits: valid date string
last 4 digits: serial number 0..9999
ex. 202308301234
上面的信息,足够分析DataName的组合数。通过这样的分析和记录,就能够很好的搞清楚领域中的信息,并记录为整个团队都能很好理解的形式。
在领域分析和设计的阶段,并不需要专注于特定的语法和开发语言,关键是要清晰地分析和表达各个值的类型,要清晰到能够分析组合数的程度。
结论
- 领域分析可以采用领域的语言和领域的知识来完成,领域、子领域、上下文;
- 领域建模的核心就是把领域的数据、流程(输入输出和映射)描述为对应的ADT;
- F#等函数式编程工具通过对ADT的支持,提供了领域模型的绝佳对应,非常适合用于领域驱动开发。