Chap10 领域模型 - 领域知识

领域模型 - 业务需求 - 概念、规则、概念之间的关系 - 统一语言可视化 - 封装,隐藏细节;抽象,提取共同特征。
了解整个系统的目标和范围,对系统的领域需求达成共识。

共识

沟通与协作 - 无法“发现”需求,而是要和客户一起“培育”需求,并在这个培育过程中逐渐成熟。
信息不同,背景不同 —— 原始的需求是“哈姆雷特”。
image.png
合同方式约定需求知识不可靠,易发生超出文档边界的变更 —— 理解偏差的存在:

  • 从客户那里了解到的需求,并非最终的需求。
  • 沟通有效性低,理解偏差。
  • 理解到的需求并没有揭示完整的领域知识。

通过可视化的交流形式逐渐在多个角色之间达成共识:
image.png
可视化的方式表现出来,例如,绘图、便签、编写用户故事或测试用例等都是重要的辅助手段。
image.png
看到需求之间的差异,互补不足去冗余,最终得到大家都一致认可的需求,形成统一的认知模型。

团队协作

项目之初,尤其是有客户参与的先启阶段,是最好的理解领域知识的方法。
建立初步的统一语言,在识别出主要的史诗级故事主要用户故事之后,进而识别出限界上下文,并建立系统的逻辑架构与物理架构。
image.png

  • 在期望与愿景的核心目标指导下,团队与客户才可能就问题域达成共同理解。
  • 我们需要确定项目的当前状态与未来状态,从而确定项目的业务范围。
  • 之后,就可以对需求进行分解了。
  • 在先启阶段,对需求的分析不宜过细,因此需求分解可以从史诗级(Epic)到主故事级(Master)进行逐层划分。
  • 并最终在业务范围内确定迭代开发需要的主故事列表。

迭代开发沟通关键点:
image.png
每一个功能的实现、每一行代码的编写都是围绕着用户故事开展的,它是构成领域知识的最基本单元

发开人员

当用户故事从需求分析人员传递给开发人员时,充分理解用户故事描述的需求后,将需求分析人员与测试人员叫过来,大家一起做一个极短时间的沟通与确认。我们称这一活动为“Kick Off” —— 应对“盲人摸象”。
开发人员多问需求分析人员“为什么”,探索用户故事带来的价值,理解业务逻辑与业务规则。
开发人员要与测试人员再三确认验收标准,以形成一种事实上的需求规约。

运用领域场景提炼领域知识

业务场景分析 6W 要素:Who、What、Why、Where、When、HoW
image.png
识别用户角色,分析用户特征和属性,辨别其在场景中参与的活动 —— 明确业务功能(What),该功能给用户带来的业务价值(Why)。
领域功能三层次(职责层次,Responsebilaty):

  • 业务价值(Why):业务价值体现职责的目的,提供职责,“用户履行了职责”,场景对用户才有价值。
  • 业务功能(What):支撑功能,实现业务价值。
  • 业务实现(hoW):深入分析功能,获得具体的如何业务实现。

    下订单

    下订单这一职责具有业务价值,职责分层结构:

  • 下订单

    • 验证订单有效
      • 合法用户
      • 信息完整
      • 商品库存
    • 验证业务规则计算订单总价、优惠与配送费
      • 促销规则
      • 订单总价
      • 订单优惠
      • 配送费
    • 提交订单
      • 插入订单、订单项
      • 订单状态
    • 更新购物车
      • 删除商品
    • 发送通知
      • 邮件通知付款

利用场景建模,考虑场景边界。校验库存量的业务实现需要库存接口,该功能属于下订单场景的边界之外(引入限界上下文)。
提炼的领域知识必须具备 6W 要素

  • 校验领域逻辑,缺乏部分要素 => 忽略了重要领域概念。
  • 按场景 6W 模型分析领域逻辑、提炼领域知识 => 保证领域模型完整性。

    领域场景分析方法

    团队和领域专家的对话常态化 —— 团队形成一种相对固定的场景分析模式 => 对话更高效。
    场景分析模式:

  • 用例(Use Case)

  • 用户故事(User Story)
  • 测试驱动开发(Test-Driven Development)
  • 用例

    Ivar Jacobson —— 用例 —— 通过某部分功能使用系统的一种具体方法(相关事务的一个具体序列,对话形式) —— 一个用例对应一个完整序列的事件。
    用例规格说明:
    用例名称:买家下订单
    用例目的:本用例为买家提供了购买心仪商品的功能。
    参与者:买家
    前置条件:买家已经登录并将自己心仪的商品添加到了购物车。

    基础流程:
    1. 买家打开购物车
    2. 买家提交订单
    3. 验证订单是否有效
    4. 计算订单总价
    5. 计算订单优惠
    6. 计算配送费
    7. 系统提交订单
    8. 删除购物车中对应的商品
    9. 系统通过电子邮件将订单信息发送给买家

    替代流程:系统验证订单无效
    在第3步,系统确认订单无效,提示验证失败原因

    替代流程:提交订单失败
    在第7步,系统提交订单失败,提示订单失败原因
    用例图可视化,要素:

  • 参与者(Actor,Who)

  • 用例(Use Case,What)
  • 用例关系:使用(use,Why)、包含、扩展、繁华、特化等。
  • 边界(Boundray,Where)

image.png
仅 place order 用例和 buyer 参与者存在使用(use)关系 —— 仅“下订单”对“买家”有业务价值 —— 领域场景主用例,其他用例为协作关系的子用例。
用例协作关系:

  • 包含(include):主用例中不可缺少的一个执行步骤,缺少 => 不完整(must have)。
  • 扩展(extend):对主用例的补充或强化,对主用例不产生直接影响,缺少 => 保持完整(nice to have)。

包含、扩展 —— 子用例都是服务于主用例 => 体现用例规格描述的流程(When and hoW)。
边界划分 —— 用例的职责相关性 —— 责任内聚(亲密程度) —— orders context、notification context、… => 用例边界(Where)。
总结:
开发团队和领域专家可视化沟通的一种方式。
用例的关注点是领域,避免陷入技术细节。
用例表达的领域概念必须精准,采用统一语言中的概念为每个用例命名(引入“局外人”对用例提问)。
然后用言简意赅的动宾短语描述用例,并提供英语表达(防止破坏统一语言)。

用户故事

软件系统最重要的核心 —— 用户
用户故事 —— 用户角度 —— 身临其境,“讲故事” —— 领域场景描述 —— 6W 模型。
模板:
As a(作为)<角色>
I would like(我希望)<活动>
so that(以便于)<业务价值>
例:
作为一名用户,
我希望可以提供查询功能,
以便于了解分配给我的任务情况。
业务目标不清晰,改进:
作为一名项目成员,
我希望获取分配给自己的未完成任务,
以便于跟踪自己的工作进度。
发起者:”项目成员”,因:“跟踪工作进度(价值)”,果:“获取分配给自己的未完成任务”。
敏捷实践 —— 需求分析人员与测试人员结对编写用户故事 —— 可测试(Testable)—— 验收标准(Acceptance Criteria),是对业务活动的细节描述,并非测试用例。
阐述验收标准的方式:Given-When-Then模式,实例化需求。
例:
作为一名销售经理
我希望为订单设置合适的配送免费的总额阈值
以便于促进平均订单总额的提高

验收标准:
订单总额的货币单位应以当前国家的货币为准
订单总额阈值必须大于0
采用 Given-When-Then 模式(hoW),并通过实例化需求的方式编写用户故事:
作为一名销售经理
我希望为订单设置合适的配送免费的总额阈值
以便于促进平均订单总额的提高

场景1:订单满足配送免费的总额阈值
Given:配送免费的总额阈值设置为95元人民币
And:我目前的购物车总计90元人民币
When:我将一个价格为5元人民币的商品添加到购物车
Then:我将获得配送免费的优惠

场景2:订单不满足配送免费的总额阈值
Given:配送免费的总额阈值设置为95元人民币
And:我目前的购物车总计85元人民币
When:我将一个价格为9元人民币的商品添加到购物篮
Then:我应该被告知如果我多消费1元人民币,就能享受配送免费的优惠
行为驱动开发(Behavior-Driven Development)—— 使用领域特定语言(Domain Specific Language,DSL)描述用户行为,编写用户故事。
核心在于“行为” —— 做了什么 —— 不是怎么做 —— 业务建模阶段,业务才是中心(勿舍本逐末)。
用户故事只受业务规则与业务流程变化的影响 —— 不受影响于 UI 改动和技术方案改动。

测试驱动开发

一种开发实践:测试优先 <= 需求分析优先 <= 任务分解优先 <= 识别职责(可验证)。
任务分解后,不应针对被测方法编写单元测试,根据领域场景编写 —— 驱动我们去寻找领域概念。
遵循 Given-When-Then 模式 —— 测试的准备、期待的行为、验收条件 —— 体现 DDD 对设计的驱动力:

  • Given 驱动我们思考被测对象创建,以及对象与其它对象的协作。
  • When 驱动我们思考被测接口的方法命名,传参,行为方式(命令式或查询式)。
  • Then 启动我们思考被测接口返回值。

一定要从业务而非实现的角度去思考接口(表达领域知识):

  • 实现角度的设计:check()
  • 业务角度的设计:guess()

Then 验证:
@Test
public void should_return_0A0B_when_no_number_is_correct() {
//given
Answer actualAnswer = Answer.createAnswer(“1 2 3 4”);
Game game = new Game(actualAnswer);
Answer inputAnswer = Answer.createAnswer(“5 6 7 8”);
//when
String result = game.guess(inputAnswer);
//then
assertThat(result , is(“0A0B”));
}
测试方法名可以足够长,清晰表达业务,用 Ruby 风格命名,以 should 开头。

提炼领域知识

贯穿 DDD 全过程,重视领域知识,维护统一语言。
双向过程:已提炼的领域知识指导识别用例,编写用户故事和测试用例 <=> 领域场景分析帮助确认领域知识,将达成共识的统一语言更新到领域知识。

建立统一语言

获取统一语言,是需求分析的过程,是团队就系统目标、范围、具体功能达成一致的过程。
体现在两方面:

  • 统一的领域术语
  • 领域行为描述

    统一的领域术语

    维护领域术语表,给出对应的英语术语,防止影响代码实现。不保证纯正,但保证一致性。
    建模之初就明确母语和英语的表达,杜绝命名不一致。

    领域行为描述

    描述要求:

  • 从领域而非实现的角度描述领域行为;

  • 遵循术语表规范
  • 动词精确性,符合业务动作在该领域的合理性
  • 要突出与领域行为有关的领域概念

例:
作为一名Scrum Master,
我希望将Sprint Backlog分配给团队成员,
以便于明确Backlog的负责人并跟踪进度。

验收标准:
被分配的Sprint Backlog没有被关闭
分配成功后,系统会发送邮件给指定的团队成员
一个Sprint Backlog只能分配给一个团队成员
若已有负责人与新的负责人为同一个人,则取消本次分配
每次对Sprint Backlog的分配都需要保存以便于查询
明确领域行为的前置条件、执行的主语宾语*执行结果

分配 Sprint Backlog 领域行为的代码实现:
package practiceddd.projectmanager.scrumcontext.domain;

public class SprintBacklog extends Entity {
private String title;
private String description;
private BacklogStatus backlogStatus;
private MemberId ownerId;

  1. public SprintBacklog(BacklogId backlogId, String title, String description) {<br /> if (title == null) {<br /> throw new InvalidBacklogException("the title of backlog can't be null");<br /> }
  2. this.id = backlogId;<br /> this.title = title;<br /> this.description = description;<br /> this.backlogStatus = new NewBacklogStatus();<br /> }
  3. public SprintBacklogAssignment assignTo(TeamMember assignee) {<br /> if (this.backlogStatus.isClosed()) {<br /> throw new InvalidAssignmentException(<br /> String.format("The closed sprint backlog %s can not be assigned to %s.", <br /> this.title, assignee.getName()));<br /> }<br /> if (assignee.isSame(this.ownerId)) {<br /> throw new InvalidAssignmentException(<br /> String.format("The sprint backlog %s not allow to assign to same team member %s.", <br /> this.title, assignee.getName()));<br /> }<br /> return new SprintBacklogAssignment(this.id, assignee.id());<br /> }<br />}<br />基于“**信息专家模式**”,assignTo() 仅完成部分领域行为(只承担能够履行的职责)。AssignSprintBacklogService 来完成整个用户故事描述的业务场景:<br />package practiceddd.projectmanager.scrumcontext.domain;

@Service
public class AssignSprintBacklogService {
@Autowired
private SprintBacklogRepository backlogRepository;
@Autowired
private SprintBacklogAssignmentRepository assignmentRepository;
@Autowired
private NotificationService notificationService;

  1. public void assign(SprintBacklog backlog, TeamMember assignee) {<br /> SprintBacklogAssignment assignment = backlog.assignTo(assignee);<br /> backlogRepository.update(backlog);<br /> assignmentRepository.add(assignment);
  2. AssignmentNotification notification = new AssignmentNotification(assignment);<br /> notificationService.send(notification.address(), notification.content());<br /> } <br />}