领域驱动设计领域建模_4_领域实现建模
前言
Github:https://github.com/HealerJean
一、稳定的领域模型
一个稳定的领域模型也是最容易执行单元测试的模型
1、菱形对称架构与测试金字塔
菱形对称架构的分层决定了它们不同的职责与设计的粒度。层次、职责和粒度的差异,恰好与测试金字塔形成一一对应的关系
如果测试编写得体,测试代码也可以认为是一份精炼文档,且这样的文档还具有和实现与时俱进的演进能力,形成一种活文档
测试方法体应遵循
Given
-When
-Then
模式。该模式清晰地描述了测试的准备、期待的行为和相关的验收条件。
Given
:为要测试的方法提供准备,包括创建被测试对象,为调用方法准备输入参数实参等。
When
:调用被测试的方法,遵循单一职责原则,在一个测试方法的When
部分,应该只有一条语句对被测方法进行调用。
Then
:对被测方法调用后的结果进行预期验证。
编写良好的单元测试本身就是“新兵训练营”的最佳教材,将其作为精炼文档用以传递领域知识好处更为明显:你无须额外为核心子领域编写单独的精炼文档,引入单元测试或者采用测试驱动开发就能自然而然收获完整的测试用例;
这些测试更加真实地体现了领域模型对象之间的关系,包括它们之间的组合与交互过程;
将测试作为精炼文档还能保证领域模型的正确性,甚至可以更早帮助设计者发现设计错误。
public class AccountTest {
private AccountId srcAccountId;
private AccountId targetAccountId;
@before
void setup(){
srcAccountId = AccountId.of("123456"); //用于演示
target AccountId = AccountId.of("654321"); //用于演示
}
@Test
void should_transfer_from_src_account_to_target_account_given_correct_transfer_amount() {
// given
Money balanceOfSrc = new Money(100_000L, Currency.RMB);
SourceAccount src = new Account(srcAccountId, balanceOfSrc);
Money balanceOfDes = new Money(0L, Currency.RMB);
TargetAccount target = new Account(targetAccountId, balanceOfDes);
Money trasferAmount = new Money(10_000L, Currency.RMB);
// when
src.transferTo(target, transferAmount);
// then
assertThat(src.getBalance()).isEqualTo(MoCurrency.RMB));
assertThat(target.getBalance()).isEqualTo(Money.of(10_000L, Currency.RMB));
}
}
二、测试优先的领域实现建模
三、领域建模过程
1、完整案例
1)案例说明
薪资管理系统的需求说明如下:
公司雇员有3种类型:钟点工、月薪雇员和销售人员。
对于钟点工,系统会按照雇员记录中每小时报酬字段的值为他们支付报酬。他们每天会提交记录了日期以及工作小时数的工作时间卡。如果他们每天工作超过8小时,超过部分会按照正常报酬的1.5倍进行支付。
月薪雇员以月薪进行支付,在雇员记录中有月薪字段。公司会对雇员做考勤处理,如果雇员迟到、早退或旷工,会扣除其月薪的一定金额。
对于销售人员,则根据他们的销售情况支付一定的报酬。他们会提交销售凭条,其中记录了销售的日期和销售产品的数量,酬金保存在雇员记录的酬金报酬字段。
在为各种类型的雇员结算薪资后,系统会根据每位雇员预留的银行账户在规定时间向其自动支付薪资。钟点工的薪资支付日期为每星期五,月薪雇员的薪资支付日期为每个月的最后一个工作日,“销售人员的薪资支付日期为每隔一星期的星期五
薪资管理系统的业务服务图
2)领域分析建模
在获得了目标系统的业务服务后,需求分析人员需要进一步细化业务服务,编写业务服务规约。如下为支付薪资的业务服务规约。
a、服务契约
服务编号:S0006
服务名:支付薪资
服务描述:
作为 财务人员(Accountant)
我想要系统按期自动 支付薪资(Salary)
以便提高财务人员的工作效率,及时发放薪资
触发事件:
每天凌晨0:00自动触发
基本流程:
1、确定 是否支付日( PayDay)
2、获取支付日对应类型的 雇员(Employee)名单
3、计算 薪资,生成雇员的 工资条(Payroll)
3.1 若为 钟点工雇员(HourlyEmployee),根据 工作时间卡(TimeCard)与时薪计算薪资
3.2 若为 月薪雇员(SalariedEmployee),根据 出勤记录 (Attendance)计算薪资
3.3 若为 销售人员(CommissionedEmployee),根据 销售凭条(Sale Receipt)计算薪资
4.向雇员的 银行账户(SavingAccount) 发起 转账,支付薪资
5.通过 邮件(Email) 通知 薪资已发放,同时 发送 工资条给员工
替换流程:
1、如果不是支付日,直接退出
4、如果薪资支付失败,给出失败原因,并发送邮件给财务人员
验收标准:
1、钟点工雇员的支付日为每星期五
2、如果钟点工雇员未提交工作时间卡,视为未工作3、工作时间卡的工作时间最低不少于1小时,最高不高于12小时
4、每天工作超过8小时,超过部分按照正常报酬的1.5倍进行结算
5、月薪雇员的支付日为每个月最后一个工作日
6、若月薪雇员的出勤记录包含旷工,将按照月薪计算出来的日薪进行扣除
7、若月薪雇员的出勤记录包含迟到、早退,将扣除日薪的20%
8、销售人员的支付日为每隔一星期的星期五
9、若销售人员未提交销售凭条,酬金报酬为0
10、会为符合支付条件的员工生成工资条
11、支付成功后,员工工资条的状态会更改为已支付
12、员工收到薪资发放的通知(Notification)
b、建模
名词建模:业务服务规约添加下划线的内容即我们识别出来的名词,检查这些名词是否符合统一语言的要求,即可快速映射为下图
动词建模:业务服务规约添加斜线的内容即我们识别出来的动词。逐个判断它们对应的领域行为是否需要产生过程数据。识别时,一定要从管理、法律或财务角度判断过程数据的必要性。
例如,“生成雇员工资条”动作的目标数据是工资条,无须记录在某时某刻生成了工资条,因为管理人员并不关心工资条是什么时候生成的,只要工资条存在,就不会产生审计问题。向雇员的银行账户发起转账,支付薪资”动作的目标数据是薪资,但在发起转账时,必须记录何时完成对薪资的支付,支付金额是多少,否则,若雇员没有收到薪资,就可能出现财务纠纷,于是识别出支付记录(Payment),它是支付行为的过程数据。
归纳抽象:通过名词和动词识别了领域模型之后,需要对这些概念进行归纳和抽象。注意,钟点工
(HourlyEmployee
)、月薪雇员(SalariedEmployee
)和销售人员(CommissionedEmployee
)虽然在类型上都是雇员(Employee
),但由于它们各有自身的业务含义,不可在领域分析模型中通过雇员对它们进行抽象,否则可能会漏掉重要的领域概念
确定关系:一旦明确了领域概念,就可进一步确定它们的关系,并检查这些关系是否隐含了领域概念。确定关系时,若能显而易见地确定关系数量,就标记出来,如钟点工 (
HourlyEmployee
) 与工作时间卡 (TimeCard
),就是明显的一对多关系。最终,快速建模法获得的领域分析模型如图所示。
c、领域分析模型
领域分析模型要受到限界上下文的约束。薪资管理系统分为员工上下文和薪资上下文,通过识别领域概念与限界上下文知识语境的关系,可以获得图所示的领域分析模型
员工上下文中的员工
Employee
与薪资上下文中的钟点工HourlyEmployee、月薪雇员SalariedEmployee和销售人员CommissionedEmployee充分体现了领域概念的知识语境,显然,员工上下文并不关心各种雇员类型的薪资计算和支付,而薪资上下文也不需要了解员工的基本信息
3)领域设计建模
a、聚合设计
毫无疑问,3种类型的雇员类都是实体类型。需要通过身份标识来管理工资条Payroll的生命周期,支付记录Payment作为支付行为的过程数据,也应被定义为实体
月薪雇员的出勤记录
Attendance
是从别的系统获得的,不需要在薪资管理系统中管理它的生命周期。对每个雇员而言,出勤记录的值相同,就可认为是同一条出勤记录,因此识别Attendance
为值对象工作时间卡TimeCard的相等性可以通过值决定(它的值包含员工ID),因此TimeCard也可以定义为值对象。销售凭条SalesReceipt则不同,同一个销售人员可能提交值相同的不同销售凭条,需要引入身份标识来区分,因此SalesReceipt定义为实体。财务Accountant是雇员的角色,定义为值对象。支付日PayDay的职责是判断当前日期是否支付日,本质上是一个领域服务。由此获得图17-10所示的领域设计模型。”
此材料可能受版权保护。