价值需求分析主要做的三个工作是:
- 识别利益相关者。利益相关者指的是与目标系统存在利益关系的人、团队或组织, 可以简单理解为目标系统的用户,或与目标系统有直接交互的人、团队或组织。
- 明确系统愿景。阐明目标系统要做什么,以及为何要做。
- 确定系统范围。确定系统问题空间的边界,明确系统什么该做,什么不该做。结合目标系统当前状态和未来状态进行判断。当前状态指的是系统的可用资源,包括业务资源、人力资源,资金资源等;而未来的状态则由业务目标、组织的战略规划和产品规划共同构成。
并非任何系统都DDD,DDD的核心是解决领域复杂性,若系统逻辑简单,功能不多,引入DDD则会得不偿失。而在进行价值需求分析后,我们便能判断是否需要通过DDD驱动系统的设计。
5.3 业务需求分析5.3.1 业务流程、业务场景、业务服务和业务规则使用业务流程、业务场景、业务服务和业务规则来表示业务需求。
业务流程:表示的是一个完整的、端对端的服务过程。
业务场景:按阶段性的业务目标划分业务流程,就可以获得业务场景。在示例-SMS中,老师修改成绩就分为了老师“提交申请单”,以及教务员“同意申请单”两个场景。
业务服务:角色主动向目标系统发起服务请求完成一次完整的功能交互,以实现业务目标。角色可以用户、策略(定时任务)或者其他系统,完整则强调的是业务服务的执行序列的所有步骤都应该是连续且不可中断的。业务服务是业务需求分析最核心,也是最基础的单元,而业务流程和业务场景是为了更好地分析出业务服务。在示例-SMS中的“同意申请单”场景中包含了两个业务服务:教务员“同意申请单”和系统“邮件通知”教务员。
业务规则:指对业务服务约束的描述,用于控制业务服务的对外行为。业务规则是业务服务正确性的基础。常见的业务规则有:a) 意如“若… , 就….” 的需求描述,比如示例-SMS中可提炼出“若成绩录入时间间隔超过一周,不予修改”;b) 具有事务性的操作。
5.3.2 子领域通过业务流程、业务场景和业务服务的梳理,基本可以分析出业务需求所需要的业务服务。然而,业务服务粒度太细,而问题空间又太大,我们需要找一个更粗粒度的业务单元,来帮助我们对业务服务进行聚类,一方面可以降低管理过多细粒度业务服务导致的额外复杂度,另一方面可以帮助领域专家和开发团队分析问题和设计方案时不至于陷入到业务细节中。而这个更粗粒度的业务单元就是子领域。
子领域的作用:
- 划分问题空间,作为业务服务分类的边界;
- 用于分辨问题空间的核心问题和次要问题。
子领域的分类:
- 核心子领域:能够体现系统愿景,具有产品差异化和核心竞争力的业务服务;
- 通用子领域:包含的内容缺乏领域个性,具有较强的通用性,例如权限管理和邮件管理;
- 支撑子领域:包含的内容多为“定制开发”,其为核心子领域的功能提供了支撑。
子领域的功能分类策略:问题空间应该分为哪些子领域,需要团队对目标系统整体进行探索,并根据功能分类策略进行分解。
- 业务职能:当目标系统运用于企业的生产和管理时,与目标系统业务有关的职能部门往往会影响目标系统的子领域划分,并形成一种简单的映射关系。这是康威定律的一种运用。
- 业务产品:当目标系统为客户提供诸多具有业务价值的产品时,可以按照产品的内容与方向进行子领域划分。
- 业务环节:对贯穿目标系统的核心业务流程进行阶段划分,然后按照划分出来的每个环节确定子领域。(这也是我们最常用的策略)
- 业务概念:捕捉目标系统中一目了然的业务概念,将其作为子领域。
划分子领域的过程存在很多经验因素,一个对该行业领域知识了如指掌的领域专家,可以在完成价值需求分析后,结合自身的领域经验,能够选择合适的聚类策略并给出稳定的子领域列表。但,没有领域经验也没有关系!因为根据知识消化循环思路,再经历多个迭代后收敛出来的子领域划分也会逐渐合理,逼急领域专家凭经验得出的子领域划分,只是可能需要的时间要长一些。
6.架构映射阶段在架构映射阶段,我们需要识别限界上下文,并通过上下文映射表示限界上下文之间的协作关系。
6.1 限界上下文的定义和特征6.1.1 限界上下文的定义限界上下文是语义和语境的边界。在问题空间,统一语言形成了团队对领域概念的统一表达,子领域形成了领域概念之间的边界。而在解空间,限界上下文可以看做是统一语言 子领域的融合体,统一语言需要在限界上下文内才具有明确的业务含义。
以电商购物场景为例。在进行商品下单后,系统会生成一个订单;在用户付款完成后,系统也会生成一个订单;到了物流派送流程,系统还会生成一个订单。虽然这三个步骤中的领域概念都叫订单,但是他们的关注点/职责却不同:商品订单关注的是商品详情,支付订单关注的是支付金额和分润情况,物流订单关注的是收货地址。也就是说,商品、支付和物流分别为三个限界上下文,而订单作为统一语言需要在特定的限界上下文内,我们才能够明确其关注点/负责的职责。
6.1.2 限界上下文的特征最小完备:限界上下文在履行属于自己的业务能力时,拥有的领域知识是完整的,无须针对自己的信息去求助别的限界上下文。
自我履行:限界上下文能够根据自己拥有的知识来完成业务能力。自我履行体现了限界上下文纵向切分业务能力的特征。
这里需要强调一下业务模块(横向切分)和限界上下文(纵向切分)的区别。业务模块不具备完整、独立的业务能力,它没有按照同一个业务变化的方向进行。而限界上下文是对目标系统架构的纵向切分,切分的依据是从业务进行考虑的领域维度。为了提供完整的业务能力,在根据领域维度进行划分时,还需要考虑支撑业务能力的基础设施实现,如与该业务相关的数据访问逻辑,以及将领域知识持久化的数据库模型,形成纵向的逻辑边界,即限界上下文边界。
稳定空间:限界上下文必须防止和减少外部变化带来的影响。
独立进化:指减少限界上下文内部变化对外界产生的影响。
上述的四个特征可以帮助我们验证识别出来的限界上下文。限界上下文划分是否合理、职责分配是否合理(最小完备 & 自我履行),是否合理运用上下文映射的手段隔离外部变化的影响(稳定空间)、是否有合理的封装,对外提供的接口是否稳定(独立进化)?
6.2 限界上下文的识别6.2.1 按业务维度识别1. 归类
按照业务相关性对业务服务进行归类,业务相关性体现为:
- 语义相关性:存在相同或相似的领域概念,对应于业务服务描述的名词,如果不同的业务服务操作了相同或相似的对象,即可认为它们存在语义相关性。
- 功能相关性:体现领域行为的相关性,业务服务是否服务于同一个业务目标。
2. 归纳
归纳是对归类后的限界上下文进行命名。给限界上下文命名的过程,实际上也是对归类是否合理的再一次复查。限界上下文的命名同样需要遵循单一职责原则,它只能代表唯一的最能体现其特征的领域概念。倘若归类不合理,命名就会变得困难,这时候我们就需要反思(遵循知识消化循环)归类是否合理,并重新设计归类。
3. 边界梳理
归类和归纳之后,限界上下文的边界基本已经确定,边界梳理则是根据限界上下文特征(最小完备、自我履行、稳定空间和独立进化)以及子领域进行微调(当然也不排除大调)。
为什么需要根据子领域进行限界上下文边界的调整?限界上下文和子领域的关系是什么?
理想的限界上下文与子领域的关系是一一对应的。上文提到,子领域是领域专家根据领域经验选择合适的功能分类策略进行划分,这个过程不会牵扯对业务服务的分析,体现的是领域专家对行业的洞见和深刻认识,可见获取子领域是一个自顶向下的过程。而限界上下文则是对业务服务进行归类、归纳、梳理和调整,最终形成一个个的边界,这是一个自下而上的过程。理想情况下,两者应该是双向奔赴的,自顶向下得到的子领域和自下而上得到的限界上下文能够完美契合!但是,现实哪有这么理想呢!所以一般情况下都需要我们进行调整,力求这两者能够一一对应。
这里就再cue一下知识消化循环。优秀的领域专家划分出来的子领域,往往能够实现与限界上下文的一一对应。这就是经验的力量!那经验是怎么来的呢?我认为是领域专家经历了无数个知识消化循环之后沉淀下来的。领域专家一开始也是小白,划分出来的子领域在映射为限界上下文之后发现不同限界之间可能存在语义重叠,角色在不同限界上下文之中履行的职责可能很相似,于是他们通过知识消化循环,不断调整限界上下文的边界,然后又通过限界上下文调整子领域。慢慢地,稳定、可复用的子领域就被沉淀下来了。因此,识别限界上下文不是一个单向的过程,而是一个根据子领域调整限界上下文,然后又根据限界上下文调整子领域的循环的过程。
6.2.2 验证
正交原则
正交性:如果两个或更多事物中的一个发生变化,不会影响其他事物,这些事物就是正交的。要破坏变化的传递性,就要保证每个限界上下文对外提供的业务服务不能出现雷同。
奥卡姆剃刀原理
“如无必要,勿增实体”。这是避免过度设计的良方,同样也是我们识别限界上下文的原则。如果对识别出来的限界上下文的准确性依然心存疑虑,比较务实的做法是保证限界上下文具备一定的粗粒度。遵循该原则,意味着当我们没有寻找到必须切分限界上下文的必要证据时,就不要增加新的限界上下文。
6.3 上下文映射限界上下文封装了分离的业务能力,上下文映射则建立了限界上下文之间的关系。上下文映射提供了各种模式(防腐层、开放主机服务、发布语言、共享内核、合作者、客户方/供应方、分离方式、遵奉者、大泥球),本质是在控制变化在限界上下文之间传递所产生的影响。
下文将提供服务的限界上下文称为“上游”上下文(U表示),消费服务的限界上下文称为“下游”上下文(D表示)。
6.3.1 防腐层引入防腐层的目的是为了隔离耦合。防腐层往往位于下游,通过它隔离上游上下文发生的变化。
6.3.2 开放主机服务开放主机服务定义公开服务的协议(亦称为“服务契约”),包括通信方式、传递消息的格式(协议),让限界上下文可以被当做一组服务访问。开放主机服务也可以视为一种承诺,保证开放的服务不会轻易做出变化。
对于进程内的开放主机服务,称为本地服务(对应DDD中的应用服务)。
对于进程间的开放主机服务,成为远程服务。根据选择的分布式通信技术的不同,又可以定义出类型不同的远程服务:
- 面向服务行为,比如基于RPC,称为提供者(Provider);
- 面向服务资源,比如基于REST,称为资源(Resource);
- 面向事件,比如基于消息中间件,称为订阅者(Subscriber);
- 面向视图模型,比如基于MVC,称为控制器(Controller);