场景驱动设计新鲜出炉了

我并非要刻意创造一个方法体系,仅仅是在领域驱动设计的大旗下,发现以“场景”为起点,会有更为系统的设计过程。设计本身会有许多驱动力,场景驱动的方式并没有超出领域驱动的范畴,只是以场景来描述会更准确。

我对场景的定义为:具有业务价值的,由参与者触发的,按照时序排列的一系列连续执行的任务过程。场景的层次与Alistair Corkburn设定的用例层次一致,可以简单分为三个层次:概要目标、用户目标和子功能。

用户目标被Corkburn形象地比喻为“海平面”,它是最重要的目标,可以认为是业务需求与系统需求的分界线。只有满足用户目标的场景才体现了业务价值,因此,位于这一层的场景才可以认为是“领域场景”。准确地说,场景驱动设计其实是领域场景驱动设计,如此才能体现通过业务来驱动设计的事实。

下图体现了场景驱动设计的关键要素:

如上图所示,场景驱动设计的关键要素为角色、职责与协作。角色即对象的角色构造型,参与领域场景活动的主要角色包括应用服务、领域服务、聚合与抽象的网关。职责的层次与任务分解相对应,而任务分解的层次又与角色构造型相对应。在完成一个领域场景时,不同角色履行不同层次的职责:

  • 应用服务:匹配领域场景,提供满足业务价值的服务接口
  • 领域服务:匹配组合任务,协调多个聚合与网关之间的协作,履行提供业务功能的领域行为
  • 聚合:匹配原子任务,履行自给自足的领域行为,提供具体的业务实现
  • 网关:匹配原子任务,抽象对外部资源的访问,封装具体的技术实现

在当前领域场景的背景下,各个对象角色履行不同层次和粒度的职责。由于场景是由参与者触发的按照时序排列的一系列连续执行的任务过程,因此可以通过时序图表达它们彼此之间的协作方式。把场景与角色、职责、协作结合起来,恰好对应于6W模型。以场景作为设计起点,利用任务分解细化场景的业务需求,明确不同层次的职责,并分配给不同角色构造型的对象,结合职责层次通过时序图表现这些对象之间的行为协作。这就是场景驱动设计的全景图。

为了简化场景驱动设计,可以将该设计方法固化为一个可按部就班执行的动态设计过程。整个设计过程如下所示:

场景驱动设计的过程分为三个步骤:

  1. 识别场景:从需求中识别出独立的具有业务价值的领域场景
  2. 分解任务:根据职责的层次对领域场景进行任务分解
  3. 分配职责:为领域驱动设计角色构造型分配不同层次的职责

场景驱动设计的这三个步骤糅合了几种方法。它的基础其实事件风暴的成果,即通过事件风暴得到的领域分析模型,其中包含了决策命令、读模型、聚合和事件。每个决策命令都是潜在的领域场景。

分解任务其实最符合设计者思维方式,这其实是一种自顶向下的设计方式,它同时也作为测试驱动开发的前置条件。我根据子任务的粒度,将这些任务分为“组合任务”和“原子任务”。任务的类别划分直接影响到后面的职责分配。

分配职责的基础是角色构造型。下图是我总结的主要角色构造型:

在场景驱动设计中,发挥重要的角色构造型包括:应用服务、领域服务、聚合和网关。它们与场景及任务存在以下对应关系:

  • 应用服务:场景,体现业务价值
  • 领域服务:组合任务,封装多个领域对象之间的协作
  • 聚合:代表领域行为的原子任务
  • 网关:访问外部资源的原子任务

分配职责时,可以借用时序图来表达多个对象角色之间的协作关系。

可以看出,分解任务是场景驱动设计中的关键。只要任务分解合理了,按照我固化的设计流程进行职责分配是水到渠成的过程。我们还可以借助一些工具来显化职责分配与对象协作。推荐我的朋友肖鹏的时序图工具ZenUml,该工具提供了程序员最容易理解和接受的伪代码形式绘制时序图。关键在于这种伪代码将任务与职责完美地融合起来了。

例如,针对信用卡开卡的领域场景,分解的任务如下所示:

  • 审核申请
    • 获得征信信息
    • 验证申请信息
    • 审核
  • 生成卡号
  • 通知申请人
    • 根据模板生成通知内容
    • 获取模板
    • 根据申请信息和模板生成通知内容
    • 发送短信

根据这些任务,将场景对应应用服务,然后将组合任务和原子任务分配给对应的角色构造型,就可以编写如下伪代码:

ApplicationAppService.approve() {
  ApprovingApplicationService.execute() {
    CreditInvestigationGatewary.retriveCredit();
    Application.validate();
    Application.approve();
    ApplicationRepository.save(application);
  }
  CreditCard.generateCardNo();
  NotifingService.execute() {
     GeneratingNotificationService.execute() {
       TemplateRepository.find();
       Notification.generate(template, application);
     }
     SmsGateway.send(notification);
  }
}

由此伪代码获得的时序图如下所示:

在得到这些伪代码之后,我们可以利用测试驱动开发由原子任务开始编写单元测试。编写时,仅针对代表领域行为的原子任务进行测试驱动。在这个过程中,需要严格遵循红-绿-重构的节奏进行,通过重构发现之前设计上的不足之处,可以让聚合内实体与值对象之间的协作能够更加的合理。