一说到领域驱动设计,有相当一部分程序员会觉得,DDD就是一种装逼的做法,使用DDD除了提高代码复杂度并没有什么作用,还有一些的观点就是,DDD并不成熟,没有成熟的框架,是一种很虚的设计模式。
有以上的观点,说明他们并不了解领域驱动设计DDD。
在软件开发中,最大问题之一便是业务人员和技术人员需要某种翻译才能交流。比如,在产品经理设计的一个电商系统中,提出了【商品】的概念,但是技术人员在实际落地的情况下,每个不同阶段对应的是【商品】的部分属性。比如
从上面可以看出,尽管都是围绕【商品】在讲,但是很多时候,并不是单单指【商品】这个概念。
DDD(Domain Driven Design)其实是一种设计思维方式,它所要解决的问题,是传统软件设计方法中产生的沟通歧义导致的问题。在实施落地过程中,要求程序员与领域专家一起基于业务讨论出一套通用语言(Ubiquitous Language)表达的领域模型,这个模型并不一定就是能够直接映射会所谓库表的实体类,但这个模型是能够消除程序员与领域专家、产品经理之间的沟通歧义问题的。程序员再根据讨论出来的模型进行实际的编码落地开发。
这里的领域专家,是一个精通业务的人儿,可以是十分了解这个业务领域的产品、也可以是十分了解这个领域的技术。总之,就是要了解业务。
领域驱动设计(DDD)是教我们如何做好软件的,同时也是教我们如何更好地使用面向对象技术的。也正因为DDD是一种设计思维方式,所以它提供的是一整套软件设计及落地的方法论,没有所谓的“标准”可言。这也是为啥会没有相关定型的一个软件框架出来,因为每个人对于DDD的实践落地时的思考不一样,写代码的时候的分块也不一样。只要符合DDD的设计理念即可。
先大致介绍了什么是领域驱动设计,领域驱动设计解决的是什么问题之后,我先来看一组领域驱动的相关术语。看不看得懂没关系,先记住就好了。DDD分为战略建模和战术建模两大块。
关于沟通歧义,我再举一个例子【收入】和【支出】。
在电商平台里,用户花钱买东西,这个动作,对于用户来说,是【支出】,但是对于商家来说这个就是【收入】
同样的,当用户来退款的时候,对于用户的财务账户来说,是【收入】,对于商家来说,这个便是【支出】。
我上面举得这个例子其实也是有歧义用词的,那就是【用户】,因为对于咱们做电商平台系统设计开发的人儿来说,消费者是咱们的【用户】,入驻咱们平台的商家也是咱们的【用户】。只是因为我上面例子的上下文比较简单,采用【用户】这个表达大家很容易就联想到【用户】是消费者。但是当复杂系统进行设计的时候,或者跨部门讨论的时候,一直用【用户】这个词就容易产生理解上的歧义。
例如在运营部门,一部分是针对消费者行为进行运营分析的,一部分是针对商家数据运营分析的,多方一起坐下来讨论的时候,都说自己是做【用户】行为分析的,如果你是一个新介入团队的人儿,你能够明白吗?我想,大多数情况下你们听懵了。然后在做【用户】行为分析的时候会把两者的各自特有的一些动作给混淆。
DDD 要求领域专家和技术人员坐在一起通力合作、密切沟通来分析和建模,领域专家对业务有着深刻的理解,技术人员擅长技术实现和架构设计,而领域专家和技术人员由于工种的差异导致交流产生障碍,开发人员满脑子是技术语言,领域专家脑子也都是业务概念,如果按照本能基于自己的专业背景进行沟通,效率太低了,即使有翻译的角色也会产生理解偏差,DDD 的一个核心原则是所有人员包括领域专家和技术的进行任何沟通都使用一种基于模型的通用语言(UL,Ubiquitous Language),在代码中也是这样。
限界上下文是一种概念上的边界,领域模型便工作于其中。也就是说,限界是领域模型的边界,上下文,是领域工作时的一个语义环境。 限界上下文和通用语言间存在一对一的关系。
限界上下文如同细胞,细胞是上下文,细胞壁是边界,细胞内的信息负责对代谢和遗传进行调控,细胞壁对细胞起着支持和保护防御的作用,控制物质进出,让对细胞有用的物质不能出来,有害的物质也不能进入细胞。而领域驱动设计中的限界上下文保证领域模型的一致性和完整性,清晰边界的控制力保证了领域的安全和稳定。
对于限界上下文的作用可能看起来还是有点抽象,就举一个很简单的例子。
【小米】这个词,如果是在3C领域,你就会知道,这个指代的是雷布斯的小米
如果是你妈妈让你去市场买【小米】,那么此时粮食领域的【小米】就是咱们吃的粗粮了
图中的虚线框就是边界(Bounded),上下文(Context)就是实体在这个领域内的语义环境
每个限界上下文规定了每个子域的一个上下文语义,子域跟子域之间的上下文数据交互翻译,就是上下文映射。用来表示这个上下文映射关系的图就是上下文映射图。
从上面咱们举的【小米】的例子可以知道,在不同的限界上下文中,同一个概念是可以表示不同的意思的。
在文章最开始的时候提到的【商品】也是在不同子域内对应的是【商品】不同的含义
上面这图只是很简单地表示的域与域之间的关系,ACL,upstream、downstream等都没表示出来。
聚合可以由单个实体(Entity)组成,也可以由一组实体和值对象(Value Object)组成。
在我的理解中,聚合形成了一个子域,有属于自己的限界上下文,聚合根就是这个子域里面的核心实体。(如果这里描述的有失偏颇,也欢迎各位专家批评指正。)
例如我们订单域就可以看作是一个聚合,每个聚合遵循高内聚低耦合,通过限界来确定聚合的边界,确定了该聚合里面应该包含哪些实体还有哪些值对象。
在时间上有连续性,并且有唯一标识可以来区分的对象。一般是业务对象,具有业务属性和业务行为。比如在电商系统里的消费者,它就是一个实体。这个实体可以进行浏览商品、下单买东西、申请退款等动作。
值对象是属性的集合,用来描述事物(实体)的。例如,消费者的昵称、年龄、性别、购买次数等等
资源库(Repository)是聚合实例持久化的地方,另外,对聚合的查找和获取也通过资源库完成。资源库将实际的存储和查询技术封装起来,对外隐藏封装了数据访问机制。
资源库与 DAO 有些相似,但也存在显著区别,DAO 是比 Repository 更低的一层,同时 DAO 只是对数据库的一层很薄的封装,而资源库则更加具有领域特征,以“领域”为中心,所描述的是“领域语言”。有可能是一个资源库操作对应多个DAO的操作。
在领域模型中,有些业务操作并不能自然地放在实体或值对象上,此时我们可以使用无状态的领域服务(Domain Service)。
领域服务处理的是领域中的对象,比如实体、值对象等
领域服务是负责对领域中一系列对象的编排处理
当我们发现一个操作无法赋予一个实体或者值对象,且该操作又对业务流程很重要时,我们往往需要使用领域服务
领域服务中的操作,从领域的角度来看,它是一个整体
例如,消费者在电商平台,下单买了一个商品,此时会通知订单域生成一个订单,仓储域要对该商品的库存进行调整。此时【商品下单】这个操作就可以作为一个领域服务进行封装
领域事件(Domain Event)表示领域模型中发生的重要事件。一般通过发布-订阅模式对于领域事件进行发布通知。
跨微服务的领域事件会在不同的限界上下文或领域模型之间实现业务协作,其主要目的是实现微服务解耦,减轻微服务之间实时服务访问的压力。
模块表示一个命名的容器,用于存放领域中内聚在一起的类,将类放在不同模块中目的在于松耦合。模块是通用语言的重要组成部分
DDD让开发者和熟悉业务的人一起工作,加强团队间不同角色的合作; 能够帮助业务人员和开发人员梳理清楚复杂的业务规则;
开发出来的软件是能够准确表达业务规则的,设计就是代码,代码就是设计;
对于系统按业务进行分域,天然就是适用于微服务架构,不同子域分而治之,也适合云原生的场景。可以根据每个子域不同的应用场景压力进行横向扩展。比如在12306的订单秒杀系统,那它订单域所在的服务器性能,以及服务升级等都可以根据节假日运输压力进行弹性的调整。
因为DDD是一种思想,而且也对各个领域进行职责分块,因此每个领域使用什么具体技术落地并不影响整体系统的功能。只要大家约定好上下文通信即可。
首先DDD是一种思想,就要求落地的人要有这种思想,否则在推行的时候就是很虚也很混乱。
这里的人是包括了系统设计人员和系统开发人员。系统设计要求对本系统进行正确的抽象,合理的划分各个领域。
这同样会对程序员的能力要求相对高一些。这里的能力不仅仅只是编码能力,还有思维能力。
DDD的实施会提高代码的复杂度。因为DDD要求每个子域做到高内聚低耦合,对外的接口都用防腐层和适配器来实现,这无疑会增加代码量和对象转换复杂度。
如果你是一个简单系统,后续业务基本不会迭代和更新维护,那么你就没有必要上DDD,因为这会增加软件系统的复杂度,增加软件系统复杂度其实也是引入了一定的系统风险。
注:图片来源网络,侵删
DDD让开发者和熟悉业务的人一起工作让大家在系统开发过程中逐渐变成领域专家。
DDD适用于业务场景复杂而且需要长期维护的系统。
在使用DDD时,我们应该采用最简单的方式对复杂领域进行建模,而不是使问题变得更加复杂。在实施DDD时,设计就是代码,代码就是设计。
多数开发者在采用DDD时都需要转变自己思考问题的方式。作为开发者,我们都是技术思想者,技术实现对于我们来说并不是什么难事。但是在做领域驱动设计的时候,要求我们使用业务领域的思想来思考和设计。因为我们设计做出来的系统,最终得取悦你的客户,而不是作为技术开发者的自己。
从客户的角度来设计领域模型是大有好处的。至少不会出现传统瀑布模型设计导致的,客户要一个苹果,最后做出来一个梨。
瀑布模型设计,开发过程是通过设计一系列阶段顺序展开的,从系统需求分析开始直到产品发布和维护,每个阶段都会产生循环反馈。软件开发的各项活动严格按照线性方式进行,当前活动接受上一项活动的工作结果,实施完成所需的工作内容。当前活动的工作结果需要进行验证,如果验证通过,则该结果作为下一项活动的输入,继续进行下一项活动,否则返回修改。
瀑布模型的突出缺点是不适应用户需求的变化。
《领域驱动设计》Eric Evans
《实现领域驱动设计》Vaughn Vernon