DDD驱动业务模型重构的粗浅实践

1,585 阅读8分钟

项目背景

本次项目的目标服务为 身份认证服务,其基本的业务过程可以抽象为: 通过权威数据源对用户身份信息的正确性进行检查和验证的服务。其中权威数据源包含银行信息、公安库信息、工商登记信息等等。

在本项目之前,身份认证服务主要提供两个核心业务价值:

  • 作为其他二方业务的前置业务,以满足二方业务对合法性的要求。即二方业务要求必须是真实存在的用户才能进行相关操作,否则可能面临法律风险。
  • 作为一种能力通过API输出给第三方使用;

第一个价值场景中,认证的目标是使一个内部账号的状态变为认证通过的;而第二种场景本质上不需要有内部账号的概念。但早期产品经理的思考是"支持客户选择使用其他业务的可能性"。 所以,在业务形态上依然要求客户先通过API创建账号,再对目标账号进行身份认证。基于这样的服务定位,重构前的服务的模型是 以账号为中心,账号发起认证,并将认证结果映射在账号状态上。此业务模型的系统结果如下:

以用户为中心的认证模型

上述模型的实现中,为了保证用户认证状态的实时性和可恢复性(即用户认证过程中断后,下次可直接从中断点继续),几乎所有业务请求都经过用户业务层进行转发。

问题

随着业务的推进,上述模型遇到了典型的几个问题:

  1. 应用层与能力层穿插用户业务,用户层已经对能力层进行了抽象和封装,导致上一层无法直接快速的完成新能力封装;

  2. 如果用户通过方式1进行认证,并录入身份信息;在用户由于信息异常切换方式2时,无法有效的对用户已录入信息的复用;

  3. 客户在不使用其他业务的情况下,希望简化业务交互流程,移除复杂的用户创建过程。

现有的业务模型无法从根本上解决客户需求,需要引入新的模型来实现业务重构。正好用它的一些理论来指导新模型的建立过程。

重构

一 建立Ubiquitous Language,扩充产品丰富度

在项目伊始,对业务的几个层次进行严格命名和统一,作为项目的统一语言,并将语言反应在服务对应的package和class、field命名上,做到Intention Revealing Interfaces,自释义命名。

信息比对: 发送用户信息到第三方数据中进行信息正确性检查,具有同步、一次性无状态的特点,使用verifyId标识一次调用。

核身认证: 包含信息比对和用户意愿确认两个阶段,面向一个真实自然实体信息的验证和意愿核实的过程,具有阶段和状态

实名认证: 针对内部账号的核身认证过程,认证完成后需要保证用户实名状态的成功刷新,包含了对用户状态的操作;

二 构建identityEntity

  1. 领域知识

领域知识主要来自产品经理对新产品模式的要求(简化):

三方客户在采集/部分采集用户信息后,通过后台发起目标用户的身份认证;

用户打开认证链接后,可根据自身的情况,选择三方已指定的认证方式中的一种或多种进行,只要一种认证结束,即可认为用户已完成认证;

用户认证完成后,需通知三方,并支持引导用户返回三方业务中;

三方可在任意时刻主动查询用户的认证状态及认证信息。

通过上述描述,直接对象有 第三方、用户、通知,间接对象有 认证流程、认证过程、认证配置;

  1. 对象分析
  • 第三方: 在领域上下文中,本质上并不关心第三方本身的信息,更多的是为了在服务被集成过程中,支持集成方业务连续性而需要的数据依赖,但为了在过程中查询第三方的认证配置和安全通信的需要,依然需要第三方的系统标识appId. 故可以暂抽象该对象为AppContextInfoEntity;

  • 用户: 虽然在业务逻辑中需要用户的参与,但业务流程中需要的实际上是用户所持有的身份信息,例如 证件号、姓名等。可以暂抽象该对象为ObjectInfoEntity。

  • 通知:其背后所映射的是对认证结果事件的处理策略,因此可以抽象出一个Event对象;

  • 认证流程: 代表着三方对特定用户发起的一次认证,其拥有发起、结束、进行中等状态,关联了用户信息等。可以识别该对象为本次项目中新增的核心对象, 根据模型语言,定义为identityEntity;

  • 认证过程: 指用户选择某一种方式后所生成的具体操作。由于模型比较复杂,而且在重构时业务模型已比较清晰,不再过多说明;

  • 认证配置: 开放给第三方以满足其业务定制的策略配置,与模型核心内容关联不大,本文忽略。

  1. 对象关系

AppContextEntity: 负责标识三方及三方业务上下文信息;

ObjectEntity: 负责标识目标用户的身份信息,为兼容老的模型,账号id信息也可作为Object的一个属性;

IdentityEntity: 业务逻辑组织的主体,拥有状态等基本属性,关联appContext和object,满足根据其唯一标识查询其属性;

StatusChangeEvent: 当identityEntity状态发生变化时,触发事件,完成三方客户、用户、周边域的异步通知等;

VerifyEntity、ProviderEntity和WillEntity为系统中原有服务,分别代表信息比对和意愿校验领域,写入进来为主要是为了后续的比对分析。

三 识别Entity、Value Object和Service

根据领域驱动设计中对Entity的定义,Entity最基本的职责是确保连续性,以便使其行为更清楚且可预测。通过Entity的标识定义出“符合什么条件才算是相同的事物”。

首先,IdentityEntity必然是一个Entity;

第二,AppContextEntity和ObjectEntity在脱离了认证业务本身,不能独立存在,而且即使没有相关信息,identityEntity依然可以存在。初看像Value Object,但从来都不会有两个identityEntity共用一个context或object。因为如果共用,数据的修改就会相互影响。所以,也把他们定义为Entity,只是在逻辑上其唯一标识依附于identityEntity,可以使用identityId作为其唯一标识;

第三,在认证流程结束时,需要通过事件机制完成三方通知和业务计费等操作,这属于identity状态变更时的活动或动作,因此把Envent操作归于Service的类型。

简化后的模型没有值对象,其实三方平台的配置信息,是一个值对象信息。

四 聚合

  1. providerEntity可以是自己聚合域内的根,以满足运营支撑后台管理供应商和配置三方使用供应商策略的引用操作;

  2. verifyEntity在信息比对业务上下文中是自己域的聚合根;三方客户可以直接通过API生成Entity;

  3. identityEntity是新模型的聚合根,contextEntity、objectInfoEntity是域内的实体,域对外的所有resporitory查询、更新等操作,都基于identityId进行。同时,外部对象不可能直接引用verifyEntity和WillEntity,因此,在这种场景下,两个Entity也属于域内对象。

五 设计输出Resporitory

涉及敏感信息,该部分略去。

五 重构结果

结果改造后:

  1. 系统三层能力都能统一向外输出,输出层次多样;
  2. 新抽象认证域封装后以identityId为核心,不再需要accountId,可满足客户简化流程的需求;
  3. 认证域与用户域仅在业务层编排上进行组合,本身不再产生耦合关系,既实现了核心业务复用,又完成了域解耦;

写在最后

标准的DDD设计,会在实际操作中更多的进行需求分析、模型讨论和建模后的复验等手段,个人认为项目中从需求分析到identityEntity的模型设计过程才是关键和难点。 但受限于自身对DDD理解的程度和项目的进度要求,并未花太多的精力在那个阶段,更多的在DDD的模型表示方法论上按照自己的理解进行了粗浅的探索,过程中很多点的设计都有些模棱两可,希望随着对DDD理解回看时能够发现更多的改进点,仅此算是对这个阶段做一个总结和记录吧。