一、前言

领域驱动设计(Domain-Driven Design,简称DDD),来源于 2004 年著名建模专家 Eric Evans 发表的其最具影响力的书籍 Domain-Driven Design - Tacking Complexity in the Heart of Software(领域驱动设计——软件核心复杂性应对之道)一书。书中提出了领域驱动设计的概念。随着互联网公司深入经济实体,业务越来越复杂,在软件开发过程中也会遇到越来越多的问题。尽管DDD看似是一种“古老”的思想,但直到近几年微服务架构的流行,领域设计才开始受到越来越多的关注。

二、初心

以业务为中心,产研团队如何高效快速迭代、应对业务的复杂

同盾内容安全服务平台围绕文图音视内容服务相关的业务衍生而来,主要针对内容安全相关模型提供 A/B 测试、实验结果评估、模型服务能力一键切换、合作方定制化配置、特殊合作方熔断限流、应用指标管理、业务数据同步检测、音视文图任务补偿、服务节点状态维护等。

本文就服务平台一期:模型A/B测试业务设计展开详细的论述,涉及A/B Test、实验结果计算、指标管理、他方应用节点状态维护、数据同步机制等能力。

图1 A/B测试业务流程

2.1 为什么要使用 DDD 设计

  • 过度耦合

业务初期,我们的功能都是很普通的 CRUD,简单清晰的业务让我们搞几个Service+Dao就完全玩的滴溜溜的。但是随着业务越来越复杂,业务逻辑的实现也越来越复杂,代码越来越繁重冗余。功能无法归一,同一逻辑的实现混淆在不同的业务中,业务逻辑渐渐模糊,仅凭几行注释已经无法识别出要实现的能力是什么,到最后只有老天知道是要干嘛。

此外,如果新需求要在此处增加一个新功能,存在的问题也都几乎相同:要么无从下手,需要研发花费大量的时间回溯代码逻辑;要么就是对功能研发评估的周期有误,认为功能很简单,应该分分钟搞定;还有,牵一发而动全身,虽然有测试可以跑case,但影响依然存在且很大。

  • 贫血导致的失忆症

有一次,一个同事问我,OO对象真的是面向对象吗?我没有回答,我知道他这么问,肯定有他的理由。他跟我说,OO设计和DDD设计相比,简直就是一坨翔。我问为什么,他跟我说:有数据的没有动作、有动作的没有数据,怎么能叫面向对象呢?

这段对话,我没有反驳,也找不到反驳的理由,在17年的时候老东家,当时华为出来算法领导兼任架构师,在架构设计的时候再推动DDD,当时我很拒绝,认为 OO 设计分层很清晰,无法适应 DDD 设计的业务 Action 在领域对象中这一方法。直到19年的时候读了张逸的《领域驱动设计实践》的时候有点感触,但在具体业务中也没有使用到,学到的东西也就上交给温柔的时光了。

回到正题。

贫血领域对象(Anemic Domain Object)是指仅用作数据载体,而没有数据行为和动作的领域对象。贫血模型(也有人称之为失血模型,这种分类方式有失血模型、贫血模型、充血模型、胀血模型)简单来说,就是domain object只有属性的getter/setter方法的纯数据类,所有的业务逻辑完全由business object来完成(又称Transaction Script),这种模型下的domain object被Martin Fowler称之为“贫血领域对象”。啥?等等,这不就是同事说的那“一坨翔”嘛?

img

就这个阶段而言,大部分人做的系统都或多或少都具有J2EE的身影,以Service/Action/Dao的分层模式进行开发,自然而然的写出了过程式代码,面向对象和面向过程可谓是你中有我我中有你,到最后,自己也搞不清楚到底是什么架构设计。使用这种模式的开发,对象只是数据的载体,没有行为和动作,以数据为中心,以数据库E-R设计为驱动。在面对复杂问题和业务时,业务状态、逻辑会散落在大量的方法中,原本的代码意图渐渐不明确,这便是贫血模型引起的失忆症

  • 简化系统的复杂性

《孙子兵法》中提出了分而治之的思想:“十则围之,五则攻之,倍则分之,敌则能战之,不若则能避之”。两汉时期300多年里,通过“以夷制夷”、“使之人自为雄,各相为战”、“以蛮夷攻蛮夷”、“师夷长技以制夷”等对边疆问题的分化瓦解、分而治之的哲学思想化解边疆危机、巩固政权。隋唐时期,通过“以夷攻夷”、“以夷制夷”和“以夷治夷”等政策在不损耗自身国力的情况下便稳定了周边的民族关系。

可见,分治的思想在解决重大问题中能发挥出极佳的效果,也是最优的解决方案。把问题空间分割为规模更小且易于处理的若干子问题。分割后的问题需要足够小,以便一个人单枪匹马就能够解决他们;其次,必须考虑如何将分割后的各个部分装配为整体。分割得越合理越易于理解,在装配成整体时,所需跟踪的细节也就越少。即更容易设计各部分的协作方式。评判什么是分治得好,即高内聚低耦合。

2.2 分析A/B Test需求迭代中出现的问题

分析当前阶段需求迭代过程中的问题,可以总结为以下几类问题:

  1. 业务逻辑提出方式算法同学,产品设计由我们后端研发设计。对需求把握不准确,研发同学容易主观判断、评估需求,产品的设计未考虑到三方用户,部门间推广、业务延展会越来越复杂。
  2. UI功能简单,业务逻辑功能复杂,涉及应用、指标、任务执行、数据上报、节点信息采集等工作,而且脱离运维系统提供支撑主动采集调用方节点信息势必会更复杂(节点下架、容器伸缩容、服务重启IP变化)。
  3. 研发团队面对需求增长和变化时,缺乏对业务逻辑的抽象,往往开发一个需要点需要改动多处,容易出错且开发效率低,代码维护性差。
  4. 需求文档和代码逻辑不匹配,线上功能的业务逻辑为什么实现成那样没有依据可查,领域知识得不到沉淀,团队得不到可持续成长。

三、领域设计

领域驱动设计当然不是架构方法,也并非设计模式。准确地说,它其实是“一种思维方式,也是一组优先任务,它旨在加速那些必须处理复杂领域的软件项目的开发”。领域驱动设计贯穿了整个软件开发的生命周期,包括对需求的分析、建模、架构、设计,甚至最终的编码实现,乃至对编码的测试与重构。

领域驱动设计强调领域模型的重要性,并通过模型驱动设计来保障领域模型与程序设计的一致。从业务需求中提炼出统一语言(Ubiquitous Language),再基于统一语言建立领域模型;这个领域模型会指导着程序设计以及编码实现;最后,又通过重构来发现隐式概念,并运用设计模式改进设计与开发质量。这个过程如下图所示:

图2 领域驱动设计过程

这个过程是一个覆盖软件全生命周期的设计闭环,每个环节的输出都可以作为下一个环节的输入,而在其中扮演重要指导作用的则是“领域模型”。这个设计闭环是一个螺旋式的迭代设计过程,领域模型会在这个迭代过程中逐渐演进,在保证模型完整性与正确性的同时,具有新鲜的活力,使得领域模型能够始终如一的贯穿领域驱动设计过程、阐释着领域逻辑、指导着程序设计、验证着编码质量。

如果仔细审视这个设计闭环,会发现在针对问题域和业务期望提炼统一语言,并通过统一语言进行领域建模时,可能会面临高复杂度的挑战。这是因为对于一个复杂的软件系统而言,我们要处理的问题域实在太庞大了。在为问题域寻求解决方案时,需要从宏观层次划分不同业务关注点的子领域,然后再深入到子领域中从微观层次对领域进行建模。宏观层次是战略的层面,微观层次是战术的层面,只有将战略设计与战术设计结合起来,才是完整的领域驱动设计。

这段来源:张逸《领域驱动设计实践》–领域驱动设计概览一章。

三、战略建模设计

构建领域模型、划分限界上下文

不应该按技术架构或者开发任务来划分限界上下文,而应该按照语义的边界来考虑。开发人员、产品人员与领域专家通过事件风暴对业务需求进行分析,建立通用的领域模型,并针对业务需求进行上下文划分。

实践:考虑产品所讲的通用语言,从中提取一些术语称之为概念对象,寻找对象之间的联系;或者从需求里提取一些动词,观察动词和对象之间的关系;将耦合的对象与功能圈在一起,观察他们内在的联系,从而形成对应的限界上下文。简言之,限界上下文应该从需求出发,按领域划分。

图3 创建A/B Test任务

图4 A/B Test 任务时长计算

在领域设计方面,我们是从0到1的过程,没有所谓的领域专家,只有算法和研发工程师,甚至产品也没有介入,因为对他们而言,这套系统偏向为技术服务,产品设计时存在很多技术盲区。对需求方(算法工程师)提出的需求梳理,(研发)做了简单的交互原型,用于在后面多次沟通确认“该系统满足我们的要求”。

在我们的ABTest服务中,

图5 A/B Test设计领域

四、战术建模设计

细化上下文

剖析上下文内部的组织关系,将业务描述中与此上下文相关的名词、动词等转化为领域设计要素。

• 将名词转化为实体、值对象并根据他们的关系设计聚合与聚合根;

• 将动词转化为应用服务、领域服务等,用于调度聚合中的实体实现业务需求;

• 识别领域事件以及需要用到的第三方服务,完成防腐层的设计;

图6 领域分层设计

图7 A/B Test 任务领域设计