【系统架构】领域驱动DDD(Domain-Driven Design)- 软件核心复杂性应对之道
前言
领域驱动设计是一个开放的设计方法体系,目的是对软件所涉及到的领域进行建模,以应对系统规模过大时引起的软件复杂性的问题,本文将介绍领域驱动的相关概念。
一.软件复杂度的根源
1.业务复杂度(软件的规模)
软件的需求决定了系统的规模。当需求呈现线性增长的趋势时,为了实现这些功能,软件规模也会以近似的速度增长。由于需求不可能做到完全独立,导致出现相互影响相互依赖的关系,修改一处就会牵一发而动全身。就好似城市的一条道路因为施工需要临时关闭,此路不通,通行的车辆只能改道绕行,这又导致了其他原本已经饱和的道路,因为涌入更多车辆,超出道路的负载从而变得更加拥堵,这种拥堵现象又会顺势向这些道路的其他分叉道路蔓延,形成一种辐射效应的拥堵现象。
2.技术复杂度(软件的结构)
结构之所以变得复杂,在多数情况下还是因为系统的质量属性决定的。例如,我们需要满足高性能、高并发的需求,就需要考虑在系统中引入缓存、并行处理、CDN、异步消息以及支持分区的可伸缩结构。倘若我们需要支持对海量数据的高效分析,就得考虑这些海量数据该如何分布存储,并如何有效地利用各个节点的内存与 CPU 资源执行运算。
从系统结构的视角看,单体架构一定比微服务架构更简单。
3.人为的因素
不存在一致性、不存在风格、也没有统一的概念能够将不同的部分组织在一起。缺少必要的注释,没有字段说明和数据字典, 意大利面条式的代码,缺乏统一编码风格,导致错综复杂和不可维护的程序。
4.需求引起的软件复杂度
需求分为业务需求与质量属性需求,因而需求引起的复杂度可以分为两个方面:技术复杂度与业务复杂度。
技术复杂度来自需求的质量属性,诸如安全、高性能、高并发、高可用性等需求,为软件设计带来了极大的挑战,让人痛苦的是这些因素彼此之间可能又互相矛盾、互相影响。例如,系统安全性要求对访问进行控制,无论是增加防火墙,还是对传递的消息进行加密,又或者对访问请求进行认证和授权等,都需要为整个系统架构添加额外的间接层,这不可避免会对访问的低延迟产生影响,拖慢了系统的整体性能。又例如,为了满足系统的高并发访问,我们需要对应用服务进行物理分解,通过横向增加更多的机器来分散访问负载;同时,还可以将一个同步的访问请求拆分为多级步骤的异步请求,再通过引入消息中间件对这些请求进行整合和分散处理。这种分离一方面增加了系统架构的复杂性,另一方面也因为引入了更多的资源,使得系统的高可用面临挑战,并增加了维护数据一致性的难度。
业务复杂度对应了客户的业务需求,因而这种复杂度往往会随着需求规模的增大而增加。由于需求不可能做到完全独立,一旦规模扩大到一定程度,不仅产生了功能数量的增加,还会因为功能互相之间的依赖与影响使得这种复杂度产生叠加,进而影响到整个系统的质量属性,比如系统的可维护性与可扩展性。在考虑系统的业务需求时,还会因为沟通不畅、客户需求不清晰等多种局外因素而带来的需求变更和修改。如果不能很好地控制这种变更,则可能会因为多次修改而导致业务逻辑纠缠不清,系统可能开始慢慢腐烂而变得不可维护,最终形成一种如 Brian Foote 和 Joseph Yoder 所说的“大泥球”系统。
以电商系统的促销规则为例。针对不同类型的顾客与产品,商家会提供不同的促销力度;促销的形式多种多样,包括赠送积分、红包、优惠券、礼品;促销的周期需要支持定制,既可以是特定的日期,如双十一促销,也可以是节假日的固定促销模式。如果我们在设计时没有充分考虑促销规则的复杂度,并处理好促销规则与商品、顾客、卖家与支付乃至于物流、仓储之间的关系,开发过程则会变得踉踉跄跄、举步维艰。
技术复杂度与业务复杂度并非完全独立,二者混合在一起产生的化合作用更让系统的复杂度变得不可预期,难以掌控。
二.控制软件复杂度的原则
1.保持结构的清晰与一致.
2.分而治之、控制规模
3.拥抱变化(变化对软件系统带来的影响可以说是无解)
除了在开发过程中,我们应尽可能做到敏捷与快速迭代,以此来抵消变化带来的影响;在架构设计层面,我们还可以分析哪些架构质量属性与变化有关,这些质量属性包括:
- 可进化性(Evolvability):有句话这么说的好的架构是进化来的,不是设计出来的。
- 可扩展性(Extensibility)
三.传统开发设计的模式
1.传统的设计分层结构:
Model层:
包含数据对象,是service操纵的对象,model层中的对象被建模成业务对象,这些对象是对DB中表的映射,一个表对应一个model,表中的字段就对应成model对象的属性,然后在加上get() / set()方法,但是并没有包含这个对象的业务上的行为,不知道它会做什么,这样就是一个很典型的贫血模式。
Dao层(数据访问层,DTO对象:数据传输对象):
Dao层主要是和数据库打交道,做数据持久化的工作,也包括一些数据过滤,为model层服务的,比如php里面的mysqli和pdo。
Service层:
公开一些接口给外部服务调用的,放置所有的服务类,它会调用Dao层去处理数据(获取设置数据)。
展现层(UI):
前端的一些业务逻辑展现,使用各种UI框架,如Layzui,smarty,twing等模版js框架去渲染页面。
Controller层:
层负责具体的业务模块流程的控制,在这层调用可以调用service层的接口来控制业务流程,也可以访问model层获取数据。
现在主流的php框架都是按照这样的分层去设计和开发,thinkphp,laveral,ci。
2.传统的设计方式和开发框架及其问题
1.由于设计或者编码的不当,核心业务逻辑容易散布在各处。
由于业务逻辑混散在各处,带来的麻烦维护很困难,有可能在model层 service层做一些业务方面的东西,或者在action里面写一些业务相关的代码,比如有些业务写在展现层,那么就要去改展现层里面的代码,写在service层就要去改service的逻辑。当想要了解这里业务逻辑的时候得看下上下文,翻阅很多类,需要追代码各个文件去看才能大概的明白。
2.过度耦合
业务初期,我们的功能大都非常简单,普通的CRUD就能满足,此时系统是清晰的。随着迭代的不断演化,业务逻辑变得越来越复杂,我们的系统也越来越冗杂。模块彼此关联,谁都很难说清模块的具体功能意图是啥。修改一个功能时,往往光回溯该功能需要的修改点就需要很长时间,更别提修改带来的不可预知的影响面。
下图是一个常见的系统耦合病例。
订单服务接口中提供了查询、创建订单相关的接口,也提供了订单评价、支付、保险的接口。同时我们的表也是一个订单大表,包含了非常多字段。在我们维护代码时,牵一发而动全身,很可能只是想改下评价相关的功能,却影响到了创单核心路径。虽然我们可以通过测试保证功能完备性,但当我们在订单领域有大量需求同时并行开发时,改动重叠、恶性循环、疲于奔命修改各种问题。
上述问题,归根到底在于系统架构不清晰,划分出来的模块内聚度低、高耦合。
问题:既然架构不清晰重构是否可以解决这些问题?
可以,但是并不能解决根本问题。一般重构都是通过在单独的类及方法级别上做一系列小步重构来完成,封装一些常用的操作,提炼出通用的代码片段。所以我们可以很容易重构出一个独立的类来放某些通用的逻辑,但是你会发现你很难给它一个业务上的含义,只能给予一个技术维度描绘的含义。这会带来什么问题呢?新来的同事并不总是知道对通用逻辑的改动或获取来自该类。显然,制定项目规范并不是好的方法,随着业务的变化在不久的将来重构还会一直继续下去。
3.贫血模式,基于数据表的设计,数据驱动(Data-Driven),所有的开发都是围绕数据表来进行的。
四.贫血模型
贫血领域对象(Anemic Domain Object)是指仅用作数据的载体,而没有行为和动作的领域对象,只有get和set方法,或者包含少量的CRUD方法,所有的业务逻辑都不包含在内。
贫血模型其实是违背了oop模式,对象有什么反应的就是属性,对象会做什么,反应在类里面对应的就是方法,传统开发中很明显看不到能做什么,不会有业务行为,get / set方式只是外部获取属性值的载体而已。
数据库有什么,模型才会反应什么,这样迫使我们先去设计数据库,这就是传统的开发思路,这就是基于数据库表的设计,导致的问题就是表中会有一些重复的多余的字段,在设计的时候也会依赖于的特定的数据库(因为是先去设计数据库),所以好的系统是不应该依赖于特定的数据库,更不应该依赖于特定数据库的存储过程,存储函数,触发器等,做到数据库无关性,当做到数据迁移等时候很方便很平滑。所以应该先去设计业务对象,就是对对象建模然后再去反向设计数据库。
缺点:
1.沟通困难,开发人员和业务人员交流语言不统一,开发用技术语言和业务沟通,而业务人员不了解技术,就是交流障碍。
2.业务逻辑不能重用,因为业务散在各个层,业务各个方法互相调用,你不知道调用哪个方法,业务后期的查找维护也比较困难,业务逻辑也会和应用逻辑的混合,业务逻辑反应的是需求,应用逻辑是和系统相关,比如业务要查询什么数据,查询的过程是业务,而如何展现在ui上是应用。
3.传统的开发是和特点的技术耦合的,如果想把业务脱离出来,去更换某种技术就很困难。
4.适应未来的变化就很有问题。
优点:
这种贫血模型的传统开发也是当前我们最常用的方法,开发速度快,开发人员容易掌握。
// 贫血模型下的实现 public class User{ private $id; private $name; ... // 省略get/set方法 } public class UserManagerService{ public function save(User user){ // 持久化操作.... } } // 保存用户的操作可能是这样 $userManagerService::getInstance()->save(user);
我的业务逻辑都是写在userManagerService中的,User只是个数据载体,没有任何行为。简单的业务系统采用这种贫血模型和过程化设计是没有问题的,但在业务逻辑复杂了,业务逻辑、状态会散落到在大量方法中,原本的代码意图会渐渐不明确,我们将这种情况称为贫血模型或者是由贫血症引起的失忆症。
// 充血模型下的实现 public class User{ private $id; private $name; ... // 省略get/set方法
//用户信息保存 public function save(User user){ // 持久化操作.... } } // 保存用户的操作可能是这样 $User::getInstance()->save(user);
更好的是采用领域模型的开发方式,将数据和行为封装在一起,并与现实世界中的业务对象相映射。各类具备明确的职责划分,将领域逻辑分散到领域对象中。继续举我们上述的例子,用户保存信息就应当放到User类中。
五.为什么选择DDD
既然上述传统开发和贫血模式有这些问题,那么有没有什么方法来解决这些问题?
解决思路
1.思想理论:基于领域驱动设计(DDD能应对复杂性与快速变化)。
2.技术实现:
1).从技术维度实现分层:遵循分层架构模式,能够在每层关注自己的事情,比如领域层关注业务逻辑的事情,仓储关注持久化数据的事情,应用服务层关注用例的事情,接口层关注暴露给前端的事情。通过‘开发主机服务’(REST服务是其中的一种)、消息模式、事件驱动 等架构风格实现.
2).业务维度:通过将大系统划分层多个上下文,关注点放在domain上,将业务领域限定在同一上下文中,可以让不同团队和不同人只关注当前上下文的开发。降低上下文之间的依赖,业务核心与特定的技术隔离开来,不依赖任何一个技术框架。
六.理解DDD概念
DDD的全称为Domain-driven Design,即领域驱动设计。是一种思维方式和概念,可以应用在处理复杂业务的软件项目中,加快项目的交付速度。下面我从领域、问题域、领域模型、设计、驱动这几个词语的含义和联系的角度去阐述DDD是如何融入到我们平时的软件开发初期阶段的。要理解什么是领域驱动设计,首先要理解什么是领域,什么是设计,还有驱动是什么意思,什么驱动什么。
1.什么是领域
领域代表的是某个范围,假如现在要做一个系统,这个系统有一些要实现的功能。那么这个系统肯定属于某个特定的领域,比如论坛是一个领域,只要你想做一个论坛,那这个论坛的核心业务是确定的,比如都有用户发帖、回帖等核心基本功能。比如电商系统,这种都属于网上电商领域,只要是这个领域的系统,那都有商品浏览、购物车、下单、减库存、付款交易,物流等核心环节,或者一个支付平台等。所以,同一个领域的系统都具有相同的核心业务,因为他们要解决的问题的本质是类似的。
因此,我们可以推断出,一个领域本质上可以理解为就是一个问题域,只要是同一个领域,那问题域就相同。所以,只要我们确定了系统所属的领域,那这个系统的核心业务,即要解决的关键问题、问题的范围边界就基本确定了。通常我们说,要成为一个领域的专家,必须要在这个领域深入研究很多年才行。因为只有你研究了很多年,你才会遇到非常多的该领域的问题,同时你解决这个领域中的问题的经验也非常丰富。领域专家的重要性对于设计良好的领域驱动设计是很重要的。在开发前理解领域知识是基础,也很重要,因为一个系统要做成什么样,里面包含哪些业务规则,核心业务关注点是什么,就要求对这个领域内的一切业务相关的知识都非常了解,如果开发一个陌生的系统,比如航空管理软件,让一个只会开发电商的程序员去写,是完全不知道从哪开始下手。
在日常开发中,我们通常会将一个大型的软件系统拆分成若干个子系统。这种划分有可能是基于架构方面的考虑,也有可能是基于基础设施的。但是在DDD中,我们对系统的划分是基于领域的,也即是基于业务的,领域的划分,一个大的领域可以划分成多个小的领域,也就是子域。
领域及子域的划分是如何进行的,如何去限定的,这个就得需要限界上下文和上下文映射图,下面待会说。
2.什么是设计
DDD中的设计主要指领域模型的设计。为什么是领域模型的设计而不是架构设计或其他的什么设计呢?因为DDD是一种基于模型驱动开发的软件开发思想,强调领域模型是整个系统的核心,领域模型也是整个系统的核心价值所在。每一个领域,都有一个对应的领域模型,领域模型能够很好的帮我们解决复杂的业务问题。
从领域和代码实现的角度来理解,领域模型绑定了领域和代码实现,确保了最终的代码实现就一定是解决了领域中的核心问题的。因为:1)领域驱动领域模型设计;2)领域模型驱动代码实现。我们只要保证领域模型的设计是正确的,就能确定领域模型可以解决领域中的核心问题;同理,我们只要保证代码实现是严格按照领域模型的意图来落地的,那就能保证最后出来的代码能够解决领域的核心问题的。这个思路,和传统的分析、设计、编码这几个阶段被割裂(并且每个阶段的产物也不同)的软件开发方法学形成鲜明的对比。
3.什么是驱动
上面其实已经提到了,就是:1)领域驱动领域模型设计;2)领域模型驱动代码实现。这个就和我们传统的数据库驱动开发的思路形成对比了。DDD中,我们总是以领域为边界,分析领域中的核心问题(核心关注点),然后设计对应的领域模型,再通过领域模型驱动代码实现。而像数据库设计、持久化技术等这些都不是DDD的核心,而是外围的东西。
领域驱动设计第一步最关键就是应该尽量先把领域模型想清楚,然后再开始动手编码,这样的系统后期才会很好维护。但是,很多项目(尤其是互联网项目,为了赶工)都是一开始模型没想清楚,一上来就开始建表写代码,代码写的非常冗余,完全是过程是的思考方式,最后导致系统非常难以维护。而且更糟糕的是,前期的领域模型设计的不好,不够抽象,如果你的系统会长期需要维护和适应业务变化,那后面你一定会遇到各种问题维护上的困难,比如数据结构设计不合理,代码到处冗余,改BUG到处引入新的BUG,新人对这种代码上手困难等。而那时如果你再想重构模型,那要付出的代价会比一开始重新开发还要大,因为你还要考虑兼容历史的数据,数据迁移,如何平滑发布等各种头疼的问题。
所以通过建立领域模型来解决领域中的核心问题,这就是模型驱动的思想。
七.DDD核心组件
1.通用语言
形成统一的领域术语,尤其是基于模型的语言概念,是沟通能够达成一致的前提。尤其是开发人员与领域专家之间,他们掌握的知识存在巨大的差异,尤其是专业性很强的业务,比如金融系统,医疗系统,它们的术语都很专业。而善于技术的开发人员关注于数据库、通信机制、集成方式与架构体系,而精通业务的领域专家对这些却一窍不通,但他们在讲解业务知识时,非常自然,这些对于开发人员来说,却成了天书,这种交流就好似使用两种不同语言的外国人在交谈。
使用统一语言可以帮助我们将参与讨论的客户、领域专家与开发团队拉到同一个维度空间进行讨论,若没有达成这种一致性,那就是鸡同鸭讲,毫无沟通效率,相反还可能造成误解。因此,在沟通需求时,团队中的每个人都应使用统一语言进行交流。
一旦确定了统一语言,无论是与领域专家的讨论,还是最终的实现代码,都可以通过使用相同的术语,清晰准确地定义领域知识。重要的是,当我们建立了符合整个团队皆认同的一套统一语言后,就可以在此基础上寻找正确的领域概念,为建立领域模型提供重要参考。
2.界限上下文(Bounded Contexts):
界限上下文是DDD中的一个核心模式,这种模式是帮助我们剥离开复杂的应用程序,将他们隔离开,形成不同的上下文边界。不同的模块有着不同的上下文,且能独立调用,而各自的模块可以有自己的持久化的,互不干扰。
在大型的应用程序中,不同的人对不同的的东西可能取相同的名字,这跟我们程序的类一样,为何我们要在外面放一个namespace在外面,其实也是形成一个边界。程序内部也是如此。例如,售楼部内的员工把商品房认为是产品(Product);但是在工程部,可能他们把刷灰和修理管道的服务叫做产品,为了消除这些歧义,可以定义一个边界,分离开两种情况,以免在系统内产生混淆。每个界限上下文根据特点,具体实现方式又不同,比如有些界限上下文基本没有业务逻辑,就是增删改查,则可以使用CRUD最简单的模式;有些界限上线文有一定的业务逻辑,但对高并发、高性能没要求,则可以使用经典DDD模式。有些界限上下文有一定的业务逻辑,而且有高性能要求,则可以使CQRS模式(命令查询职责分离(Command Query Responsibility Segregation,简称CQRS))。
上面这张图展示了界限上下文
举个例子,比如电商系统中订单模块的上下文有商品,物流模块上下文有货物,库存模块上下文有存货等等,这时候你会发现其实他们都是指的同一个东西,只不过在不同的上下文中被人为的赋予了不同的概念。这样是不是就更好理解界限上下文了。
3.实体:
有业务生命周期,采用业务标识符进行跟踪。比如一个订单就是实体,订单有生命周期的,而且有一个订单号唯一的标识它自己,如果两个订单所有属性值全部相同,但订单号不同,也是不同的实体。
实体之间的关系:
1)关系越多,耦合越大。
2)找出整个业务期间的都依赖的关系,某些关系只是在对象创建的时候有意义,比如创建订单的时候会查询一下商品价格信息。
3)尽可能的简化关系,避免双向依赖关系。
4.值对象:
无业务生命周期,无业务标识符,通常用于描述实体。比如订单的收货地址、订单支付的金额等就是值对象。
根据上下文的不同,一个值对象在一个界限上下文中上值对象,到了另一个界限上下文环境中会是实体,就是在不同的领域里属性是不一样的,具体还得根据上下文来看。
值对象是不可变(只读),这样线程安全,可以到处传递。
值对象最大的好处在于增加了代码复用。
5.领域服务:
无状态,有行为,服务本身也是对象,但它却没有属性(只有行为),因此说是无状态,通常负责协调多个领域对象的操作来完成一些功能。比如尝试如何将信息转化为领域模型,但并非所有的点我们都能用Model来涵盖。对象应当有属性,状态和行为,但有时领域中有一些行为是无法映射到具体的对象中的,我们也不能强行将其放入在某一个模型对象中,而将其单独作为一个方法又没有地方,此时就需要服务。协调聚合之间的业务逻辑,并且完成用例,表示某种能力。
6.聚合:
聚合是一组相关的对象,它通过定义对象之间清晰的所属关系和边界来实现领域模型的内聚,并避免了错综复杂的难以维护的对象关系网的形成,我们把聚合看作是一个修改数据的单元,目的将这些对象作为一个单元(是业务的一个最小单元,持久化最小单元),每个聚合都有一个边界和一个根,边界定义了聚合里应该包含什么,根是聚合中唯一可以被外部饮用的元素,比如说不能直接绕过订单实体去访问订单项,但在聚合边界内部,可以互相引用。聚合根具有全局唯一标识,聚合根由仓储负责持久化其生命周期,而实体只有在聚合内部有唯一局部标识,由聚合根负责其生命周期持久化。
通常将多个实体和值对象组合到一个聚合中来表达一个完整的概念,比如订单实体、订单明细实体、订单金额值对象就代表一个完整的订单概念,而且生命周期是相同的,并且需要统一持久化到数据库中。
7.聚合根:
将聚合中表达总概念的实体做成聚合根,比如订单实体就是聚合根,对聚合中所有实体的状态变更必须经过聚合根,因为聚合根协调了整个聚合的逻辑,保证一致性。当然其他实体可以被外部直接临时查询调用。
8.仓储:
用于对聚合进行持久化,通常为每个聚合根配备一个仓储即可。仓储能够很好的解耦领域逻辑与数据库。
9.工厂:
用于创建复杂的领域对象,能够将领域对象复杂的创建过程保护起来,可以创建实体,值对象。在大型系统中,实体和聚合通常是很复杂的,这就导致了很难去通过构造器来创建对象,工厂就决解了这个问题,其实就是一种封装,隐藏了复杂的创建细节。
10.上下文映射图
如果我们将限界上下文理解为是对工作边界的控制,则上下文之间的协作实则就是团队之间的协作,高效的团队协作应遵循“各司其职、权责分明”的原则。从组织层面看,需要预防一个团队的“权力膨胀”,导致团队的“势力范围”扩大到整个组织。从团队层面,又需要避免自己的权力遭遇压缩,导致自己的话语权越来越小,这中间就存在一个平衡问题。映射到领域驱动设计的术语,就是要在满足合理分配职责的前提下,谨慎地确保每个限界上下文的粒度。职责的合理分配,可以更好地满足团队的自组织或者说自治,但不可能做到“万事不求人”,全靠自己来做。如果什么事情都由这一个团队完成,这个团队也就成为无所不能的“上帝”团队了。上下文映射展现了一种组织动态能力(Organizational Dynamic),它可以帮助我们识别出有碍项目进展的一些管理问题。”这也是我为何要在识别上下文的过程中引入项目经理这个角色的原因所在,因为在团队协作层面,限界上下文与项目管理息息相关。
领域驱动设计根据团队协作的方式与紧密程度,定义了五种团队协作模式:
1)合作关系(Partnership):两个上下文紧密合作的关系,互相联系紧密。
2)共享内核(Shared Kernel):两个上下文依赖部分共享的模型。
3)客户方-供应方开发(Customer-Supplier Development):正常情况下,这是团队合作中最为常见的合作模式,体现的是上游(供应方)与下游(客户方)的合作关系。这种合作需要两个团队共同协商。
4)遵奉者(Conformist):下游限界上下文对上游限界上下文模型的追随,做出遵奉模型决策的前提是需要明确这两个上下文的统一语言是否存在一致性,因为限界上下文的边界本身就是为了维护这种一致性而存在的。
5)分离方式(Separate Ways):在典型的电商网站中,支付上下文与商品上下文之间就没有任何关系,二者是“分离方式”的体现。
八.DDD系统的分层架构
分层就是将具有不同职责的组件分离开来,组成一套层内部高聚合,层与层之间低耦合的软件系统,领域驱动设计的讨论同样也是建立在层模式的基础上的,但与传统的分层架构相比,它更注重领域架构和技术架构的分离。
领域驱动设计将软件系统分为四层:基础结构层、领域层、应用层和表现层。与上述的三层相比,数据访问层已经不在了,它被移到基础设施层了,这些是属于外围的,不是核心。
从上图还可以看到,表现层与应用层之间是通过数据传输对象(DTO)进行交互的,数据传输对象是没有行为的POCO对象,它的目的只是为了对领域对象进行数据封装,实现层与层之间的数据传递。为何不能直接将领域对象用于数据传递?因为领域对象更注重领域,而DTO更注重数据。不仅如此,由于“富领域模型”的特点,这样做会直接将领域对象的行为暴露给表现层。
领域层是业务的核心,所有的业务都是在领域层。应用层是在领域层之上,为ui服务,它是响应ui的请求去领域层调用相应的服务,把结果返回给ui,这里面包括一些事务,分页等和业务无关的,所以这些和业务无关都会放在应用层。基础设施层是服务领域层的。
架构风格
针对DDD的架构设计,《实现领域驱动设计》书中提到了几种架构风格:六边形架构、REST架构、CQRS、事件驱动等。在实际使用中,落地的架构并非是纯粹其中的一种,而很有可能户将上述几种架构风格结合起来实现。
所谓的六边形架构,其实是分层架构的扩展,原来的分层架构通常是上下分层的,比如常见的MVC模式,上层是对外的服务接口,下层是对接存储层或者是集成第三方服务,中层是业务逻辑层。我们跳出分层的概念,会发现上面层和下面层其实都是端口+适配器的实现,上面层开放http/tcp端口,采用rest/soap/mq协议等对外提供服务,同时提供对应协议的适配器;下层也是端口+适配器,只不过应用程序这时候变成了调用者,第三方服务或者存储层提供端口和服务,应用程序本身实现适配功能。
领域驱动设计(Domain Driven Design)有一个官方的sample工程,名为DDDSample,官网:http://dddsample.sourceforge.net/,该工程给出了一种实践领域驱动设计的参考架构。下图就是它的代码结构。
各个目录含义:Infrastructure(基础实施层),Domain(领域层),Application(应用层),Interfaces(表示层,也叫用户界面层或是接口层),config(各种配置)
领域驱动设计过程中使用的模式
九.领域驱动总结
DDD与数据库设计不同:
1.领域驱动设计是一种面向对象的设计,先建模再去设计数据库。
2.领域驱动设计主要是基于现实业务中的模型,更加贴近真实业务,不仅仅是一种技术的实现。
3.领域驱动设计出来的产品---领域对象(Domain Object),是一个充血模型,不但包含业务对象的属性,也包含业务对象的方法和行为,更加符合oo原则。
4.领域驱动设计并不包含数据库具体设计,而是和领域专家一起,采用统一的语言分析领域对象的属性,业务方法,以及领域之间的关系,并为之建模。
5.领域驱动设计能减少沟通的成本。
DDD的特点:
1.统一语言,业务 产品 技术交流都不会设计到具体的技术方面,主要是对核心业务的建模,不会先考虑数据表的设计,先考虑建模。
2.专有的领域层,领域层除了业务之外不设计软件架构,等底层技术。
3.领域层代码就是业务文档,看到领域层代码就能看到业务 的核心,就是从对象中不仅仅看到属性还能可以看到业务。
DDD的一些问题
1.为什么DDD可以应对复杂性?
答:就是分而自治思想,比如说一个系统几百张表,不可能一下子弄清楚,但是可以按业务,模块去划分,DDD里面叫做界限上下文,和模块(但是提出了更多概念,比如聚合,一个模块有可能还是很大。)类似,划分成一个个领域,而领域模型有清晰的边界,同时DDD重构了设计模式 架构模式 它里面也引入了ioc,工厂模式 策略模式 只是在更高层次上的应用。
2.为什么可以快速应对变化?
当问题空间出现变化的时候,我们可以快速的找到领域模型。领域层是可以很容易将业务模块拿出来重用的。
何时考虑使用领域驱动设计?
1.如果系统只是简单的curd,没有很复杂的业务逻辑,不需要领域驱动设计,反而回增加复杂性。
2.如果你的应用多于用例场景,你的系统可能会逐渐成为一个大泥球(混杂在一起的上下文关系,边界不清晰,代码混乱)。如果你确定你的系统将会更复杂,你应该使用领域驱动设计来处理这个复杂性。
使用领域驱动的难点
应用领域驱动设计并没那么简单容易,这需要花费时间和精力去了解业务领域、术语、调查、和领域专家一起合作去划分如何去划分领域,划分好边界并建模,去业务进行抽象,这也是DDD的最重要的地方。
十.一些相关的扩展阅读
CQRS架构:
核心思想是将应用程序的查询部分和命令部分完全分离,这两部分可以用完全不同的模型和技术去实现。比如命令部分可以通过领域驱动设计来实现;查询部分可以直接用最快的非面向对象的方式去实现,比如用SQL。这样的思想有很多好处:
1) 实现命令部分的领域模型不用经常为了领域对象可能会被如何查询而做一些折中处理;
2) 由于命令和查询是完全分离的,所以这两部分可以用不同的技术架构实现,包括数据库设计都可以分开设计,每一部分可以充分发挥其长处;
3) 高性能,命令端因为没有返回值,可以像消息队列一样接受命令,放在队列中,慢慢处理;处理完后,可以通过异步的方式通知查询端,这样查询端可以做数据同步的处理。
事件溯源(Event Sourcing):
基于DDD的设计,对于聚合,不保存聚合的当前状态,而是保存对象上所发生的每个事件。当要重建一个聚合对象时,可以通过回溯这些事件(即让这些事件重新发生)来让对象恢复到某个特定的状态;因为有时一个聚合可能会发生很多事件,所以如果每次要在重建对象时都从头回溯事件,会导致性能低下,所以我们会在一定时候为聚合创建一个快照。这样,我们就可以基于某个快照开始创建聚合对象了。
DCI架构:
DCI架构强调,软件应该真实的模拟现实生活中对象的交互方式,代码应该准确朴实的反映用户的心智模型。在DCI中有:数据模型、角色模型、以及上下文这三个概念。数据模型表示程序的结构,目前我们所理解的DDD中的领域模型可以很好的表示数据模型;角色模型表示数据如何交互,一个角色定义了某个“身份”所具有的交互行为;上下文对应业务场景,用于实现业务用例,注意是业务用例而不是系统用例,业务用例只与业务相关;软件运行时,根据用户的操作,系统创建相应的场景,并把相关的数据对象作为场景参与者传递给场景,然后场景知道该为每个对象赋予什么角色,当对象被赋予某个角色后就真正成为有交互能力的对象,然后与其他对象进行交互;这个过程与现实生活中我们所理解的对象是一致的;
DCI的这种思想与DDD中的领域服务所做的事情是一样的,但实现的角度有些不同。DDD中的领域服务被创建的出发点是当一些职责不太适合放在任何一个领域对象上时,这个职责往往对应领域中的某个活动或转换过程,此时我们应该考虑将其放在一个服务中。比如资金转帐的例子,我们应该提供一个资金转帐的服务,用来对应领域中的资金转帐这个领域概念。但是领域服务内部做的事情是协调多个领域对象完成一件事情。因此,在DDD中的领域服务在协调领域对象做事情时,领域对象往往是处于一个被动的地位,领域服务通知每个对象要求其做自己能做的事情,这样就行了。这个过程中我们似乎看不到对象之间交互的意思,因为整个过程都是由领域服务以面向过程的思维去实现了。而DCI则通用引入角色,赋予角色以交互能力,然后让角色之间进行交互,从而可以让我们看到对象与对象之间交互的过程。但前提是,对象之间确实是在交互。因为现实生活中并不是所有的对象在做交互,比如有A、B、C三个对象,A通知B做事情,A通知C做事情,此时可以认为A和B,A和C之间是在交互,但是B和C之间没有交互。所以我们需要分清这种情况。资金转帐的例子,A相当于转帐服务,B相当于帐号1,C相当于帐号2。因此,资金转帐这个业务场景,用领域服务比较自然。有人认为DCI可以替换DDD中的领域服务,我持怀疑态度。
四色原型分析模式:
1) 时刻-时间段原型(Moment-Interval Archetype)
表示在某个时刻或某一段时间内发生的某个活动。使用粉红色表示,简写为MI。
2) 参与方-地点-物品原型(Part-Place-Thing Archetype)
表示参与某个活动的人或物,地点则是活动的发生地。使用绿色表示。简写为PPT。
3) 描述原型(Description Archetype)
表示对PPT的本质描述。它不是PPT的分类!Description是从PPT抽象出来的不变的共性的属性的集合。使用蓝色表示,简写为DESC。
举个例子,有一个人叫张三,如果某个外星人问你张三是什么?你会怎么说?可能会说,张三是个人,但是外星人不知道“人”是什么。然后你会怎么办?你就会说:张三是个由一个头、两只手、两只脚,以及一个身体组成的客观存在。虽然这时外星人仍然不知道人是什么,但我已经可以借用这个例子向大家说明什么是“Description”了。在这个例子中,张三就是一个PPT,而“由一个头、两只手、两只脚,以及一个身体组成的客观存在”就是对张三的Description,头、手、脚、身体则是人的本质的不变的共性的属性的集合。但我们人类比较聪明,很会抽象总结和命名,已经把这个Description用一个字来代替了,那就是“人”。所以就有所谓的张三是人的说法。
4) 角色原型(Role Archetype)
角色就是我们平时所理解的“身份”。使用黄色表示,简写为Role。为什么会有角色这个概念?因为有些活动,只允许具有特定角色(身份)的PPT(参与者)才能参与该活动。比如一个人只有具有教师的角色才能上课(一种活动);一个人只有是一个合法公民才能参与选举和被选举;但是有些活动也是不需要角色的,比如一个人不需要具备任何角色就可以睡觉(一种活动)。当然,其实说人不需要角色就能睡觉也是错误的,错在哪里?因为我们可以这样理解:一个客观存在只要具有“人”的角色就能睡觉,其实这时候,我们已经把DESC当作角色来看待了。所以,其实角色这个概念是非常广的,不能用我们平时所理解的狭义的“身份”来理解,因为“教师”、“合法公民”、“人”都可以被作为角色来看待。因此,应该这样说:任何一个活动,都需要具有一定角色的参与者才能参与。
用一句话来概括四色原型就是:一个什么什么样的人或组织或物品以某种角色在某个时刻或某段时间内参与某个活动。 其中“什么什么样的”就是DESC,“人或组织或物品”就是PPT,“角色”就是Role,而”某个时刻或某段时间内的某个活动"就是MI。
以上这些东西如果在学习了DDD之后再去学习会对DDD有更深入的了解,但我觉得DDD相对比较基础,如果我们在已经了解了DDD的基础之上再去学习这些东西会更加有效和容易掌握。
十一 .其他的软件开发模式
TDD:测试驱动开发(Test-Driven Development)
BDD:行为驱动开发(Behavior Driven Development)
ATDD:验收测试驱动开发(Acceptance Test Driven Development)