文章目录
DDD如何应对软件核心复杂性?
对于一个系统,如果提前设计,都不难实现。但是就是因为没有做提前设计,就只能将这些设计上的变革延后到项目建设过程中。而此时,项目带上了沉重的业务包袱,就会让架构调整越来越困难。直到最后,这些业务包袱越来越沉重时,很多项目组就不得不走入重构的深渊。
表面上看,这似乎是技术架构的问题。如果架构师经验足够丰富,就能提前考虑到这些问题。如果开发团队技术足够过硬,就可以最快速度坚决这些问题。但是,如果你将视野放开,放到到整个项目团队。你就会发现,一味的要求技术人员能够未卜先知,这是不现实也是不理智的。尤其对于长期建设的大型项目,更是看如此。难道你要求当年开发淘宝时就要支持好双十一吗?另外,在Eric Evans提出DDD真个思想时,在书本的前言部分,就分享了对他影响很大的三个典型项目。这些项目,是无数软件团队面临问题的缩影。
有兴趣去看一下《领域驱动设计-软件核心复杂性应对之道》一书的前言部分。
所以,这一类问题,其实并不是个例,而是软件发展过程中,大家一直都在面临的通用问题。而这些问题,随着微服务架构的不断完善,开始变得越来越严重,也被越来越多的人所重视。其实很多软件团队都在尝试解决这样的问题,你可以看到,业界不断推出像敏捷开发这类的软件工程管理方式,也不断诞生Jira、Jenkins等软件实施工具。这些方式都有助于缓解这些软件的核心复杂性问题。
DDD也是随着这些核心问题逐渐诞生的一种理论体系。特别之处在于,他尝试以一种通用的、统一的理论指导,来应对软件面临的这些核心复杂性问题。以往的这些解决方案,更多的局限于某一部分架构师、某一些具体项目。这些经验很难形成团队的共识。而DDD希望让整个软件团队都用统一的方式思考解决问题,减少架构师、程序员、产品经理、测试人员等各个参与角色之间的分歧。上到企业战略设计,下到每一个功能实现,都能从DDD中获取到足够的指导。
那么DDD是如何应对这些软件核心复杂性呢?固然DDD有非常多的理论工具,这些在后面会带大家继续回顾。但是,更重要的是,DDD有两个非常核心的思想,很容易对大家习惯的软件开发方式形成冲击。
技术主动理解业务
技术虽然是为了解决业务问题而生,但是,技术却往往并不能忠实的反应业务现状。技术人员总是习惯性的按照Controller、Service等等类似的技术概念来组织整个项目。但是这些概念并不具备实际的业务价值。因此,业务专家设计出来的业务流程,总是需要产品人员进行梳理,及时雨人员设计才能最终落地到项目当中。而这其中,不可避免的会产生信息不对称的情况。尤其在面对频繁变化的复杂业务时,这种问题会更加明显。
DDD则是主动强调让技术贴近业务。主动放下技术层面的身段,让软件项目,按照业务方式来构建。业务如何运行,软件就如何构建。实际的业务实施过程中需要哪些角色,软件就构建对应的组件。这些角色之间需要怎样的联系,软件中就构建对应的关联关系。当然,这并不意味着软件可以随着业务场景随心所欲的设计。而是为了让不怎么懂技术的业务专家也能主动参与到软件建设的整个过程当中,而不仅仅是前期的需求设计。
在DDD的实践过程当中,有很多技术人员容易陷入以往的技术思维中。将DDD理解成某种技术架构。觉得只要调整一下软件架构,将DDD中的各种基础概念落地实现,就是实现了DDD。比如说,DDD中设计了Repository仓库组件,负责实体的数据持久化与查询工作。而我见过很多人,将MVC中的DAO转换成为OracleRepository,MySQLRepository等等这样的仓库实现。这些组件描述的依然是软件项目的技术实现,而不是业务实现。领域专家看到项目中都是一些这样的实现,绝大概率是一脸懵逼,从而放弃了继续参与软件建设的信心。而相反,OrderRepository,ClientRepository这一类的实现,体现的是更多的业务属性。领域专家看到这样的组件,即便不了解实现细节,也能够知道这些组件是干什么的,从而有可能继续深入的参与软件建设过程。你可以想象,如果在这种氛围中开发软件,即便真的需要重构,也比较容易得到领域专家的理解。
技术主动去理解业务,这是DDD相对于传统MVC最为明显的区别。当然,要想让技术真正能够理解业务,光靠调整软件的包结构肯定是不够的,还需要一系列从战术落地到战略设计的完整理论体系来进行完善。而这,就是接下来我们需要了解的DDD领域驱动设计。
“刚刚好”解决问题。
在以往的设计过程中,为了更多的兼容日后的需求变化,技术人员往往会做很多的“提前设计”。寄希望于这些“提前设计”能够减少未来业务变化对软件架构的冲击。但是,往往项目未来的需求变化并不是技术人员能够把控的。如果未来的业务需求不是按照之前设计的路线演进,这些“提前设计“不但无法起到设计时的作用,反而会增加软件项目的复杂度,从而给软件后续的升级改造带来更多的负担。
而DDD强调的则是让软件可以”刚刚好“的解决眼前的业务问题,同时,通过一系列的手段,为软件保持足够的灵活性。
概念
DDD全称是Domain-Driven-Design,领域驱动设计。是在2004年由Eric Evans(NoSQL概念提出者)提出的一种架构思想。DDD在诞生之后的很长一段时间内,并没有引起业界太多的关注,而直到2014年之后,微服务技术大行其道后,DDD才开始在业界火了起来。去哪儿网已经开始全面支持DDD, 爱奇艺、阿里等很多互联网企业都大量分享过关于DDD在企业内落地的经验。
DDD主要分为两个部分,战略设计与战术设计,战略设计围绕微服务拆分,战术设计围绕微服务构建
通用语言 - 定义上下文的含义
- 定义:提炼领域知识的产出物,体现在两个方面:① 统一的领域术语;②领域行为描述。
- 如何获取:统一语言就是需求分析的过程,也是团队中各个角色就系统目标、范围与具体功能达成一致的过程。
- 强调统一:无论是与领域专家的讨论,还是最终的实现代码,都使用相同的术语。
- 强调约束:既要有内涵也要有外延。
定义上下文的含义
:在事件风暴中,通过团队交流达成共识的,能简单清晰准确地描述业务含义和规则的言语就是通用语言。
注意
:通用语言贯穿 DDD 的整个设计过程。作为项目团队沟通和协商形成的统一语言,在说某通用语言时,必须要限定在某个上下文内,以确保每个上下文含义在它特定的边界内都有唯一的含义。
领域和子域 - 确定逻辑边界
领域
@汉语词典:“领域是从事一种专门活动或事业的范围、部类或部门。”
@百度百科:“具体指一种特定的范围或区域。”
两个解释有一个共同点:范围
。领域就是用来确定范围的,范围即边界,这也是 DDD 在设计中不断强调边界的原因。
在研究和解决业务问题时,DDD 会按照一定的规则将业务领域进行细分,当领域细分到一定的程度后,DDD 会将问题范围限定在特定的边界内,在这个边界内建立领域模型,进而用代码实现该领域模型,解决相应的业务问题。简言之,`DDD 的领域就是这个边界内要解决的业务问题域。
子域
概念
领域按照一定的业务规则细分,进而划分出多个子域,每个子域对应一个更小的业务范围。
过程
把问题域逐步分解,降低业务理解和系统实现的复杂度。
分类
核心域
:唯一的定义明确的领域模型,业务的核心部分,公司核心竞争力。
支撑域
:具有企业特性,具备"定制开发"的特性。
通用域
:系统中用到的通用系统,例如:认证,权限等。甚至可以采购现成的。
限界上下文(Bounded Context) - 定义领域边界的利器
我们可以将限界上下文拆解为两个词:限界和上下文。限界就是领域的边界,而上下文则是语义环境。通过领域的限界上下文,我们就可以在统一的领域边界内用统一的语言进行交流
限界上下文
:用来封装通用语言和领域对象,提供上下文环境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。这个边界定义了模型的适用范围,使团队所有成员能够明确地知道什么应该在模型中实现,什么不应该在模型中实现。
上下文映射图(Context Mapping) - 集成
上下文映射图的英文是 Context Map 其实这个翻译挺难理解的,上下文映射图其实就是不同上下文是如何进行交流的关系。由于上下文映射图内容比较少。以下内容摘自《领域驱动设计精粹》。
三种集成方式
- RPC 方式
- 消息队列或者发布 - 订阅机制
- RESTful 方式
上下文映射的种类
- 合作关系
合作关系存在于两个团队之间。每个团队各自负责一个限界上下文。两个团队通过互相依赖的一整套目标联合起来形成合作关系。一损俱损,一荣俱荣。由于相互之间的联系非常紧密,他们经常会对同步日程安排和相互关联的工作。他们还必须使用持续集成对保持集成工作协调一致。
- 共享内核
两个或者多个团队之间共享着一个小规模但却通用的模型。团队必须就要共享的模型元素达成一致。有可能他们当中只有一个团队会维护,构建及测试共享模型的代码。
- 客户 - 供应商
两个限界上下文中,一方是供应商处于上游,一方是客户方处于下游。支配这种关系的是供应商,因为它必须提供客户需要的东西。客户需要与供应商共同制订规划来满足各种预期,但最终却还是由供应商来决定客户获得的是什么以及何时获得。
- 跟随者
上游团队没有任何动机去满足下游团队的具体需求。由于各种原因,下游团队也无法投入资源去翻译上游模型的通用语言来适应自己的特定需求,因此只能顺应上游的模型。例如当一个团队需要与一个非常庞大复杂的模型集成,而且这个模型已经非常成熟时,团队往往会成为它的跟随者。
- 防腐层
这是最具防御性的上下文映射关系,下游团队在其通用语言(模型)和位于它上游的通用语言(模型)之间创建了一个翻译层。防腐层隔离了下游模型与上游模型,并完成了两者之间的翻译。所以,这也是一种集成方式。
- 开放主机服务
开放主机服务会定义一套协议或者接口,让限界上下文可以被当做一组服务访问。该协议是开放的,所有需要与限界上下文进行集成的客户端都可以相对轻松地使用它。通过应用程序编程接口提供的服务都有详细的文档,用起来也很舒服。
DDD的战术设计
总体设计思路:面向对象。
实体和值对象
在 DDD 中,实体和值对象是很基础的
领域对象
。实体一般对应业务对象,它具有业务属性和业务行为;而值对象主要是属性集合,对实体的状态和特征进行描述。但实体和值对象都只是个体化的对象,它们的行为表现出来的是个体的能力。
实体(Entity)
- 定义:
DDD 中的一类对象,拥有唯一标识符
,经历各种状态变更后仍然可以保持一致,对这类对象而言,重要的是延续性
和标识
,(对象的延续性和标识可以超出软件的生命周期)而非属性。
- 特点:
具备id标识,可以通过id进行相等性比较,实体在聚合内唯一,但是状态可变,它依附于聚合根,它的生命周期由聚合根管理,实体一般都会持久化,跟数据持久化对象存在多种对应关系(一对一,一对多,多对一,1对0),实体可以引用聚合中的聚合根,实体,值对象。
值对象(Value Object)
- 定义:
通过对象的属性值来识别的对象是值对象,它将多个相关属性
组合为一个概念整体
。它是没有标识符的对象
。
- 特点:
值对象描述了领域中的一件东西,这个东西是不可变的,无生命周期,用完即失效,值对象之间通过属性值判断相等性,他的核心是值,是一组概念完整的属性集合,用于描述实体的特征和状态,值对象尽量只引用值对象。
简单来说: 值对象本质就是一个集合。
- 意义:
领域建模过程中,值对象可以保证属性归类的清晰和概念的完整性。
聚合和聚合根
聚合
- 定义:
领域模型内的实体和值对象就好比个体,而能让实体和值对象协同工作的组织就是聚合,它用来确保这些领域对象在实现共同的业务逻辑时,能保证数据的一致性。
- 作用:
① 确保领域对象在实现共同的业务逻辑时,能保证数据的一致性
。
② 聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化
。
③ 实现微服务的"高内聚、低耦合"
:聚合有一个聚合根和上下文边界,这个边界根据业务单一职责和高内聚原则,定义了聚合内部应该包含哪些实体和值对象,而聚合之间的边界是松耦合的。
- 特点:
高内聚、低耦合,它是领域模型中最底层的边界,可以作为拆分微服务的最小单位,但我不建议你对微服务过度拆分。但在对性能有极致要求的场景中,聚合可以独立作为一个微服务,以满足版本的高频发布和极致的弹性伸缩能力。
一个微服务可以包含多个聚合,聚合之间的边界是微服务内天然的逻辑边界。有了这个逻辑边界,在微服务架构演进时就可以以聚合为单位进行拆分和组合了,微服务的架构演进也就不再是一件难事了。
聚合根
- 定义:
如果把聚合比作组织,聚合根则是组织的负责人,聚合根也叫做根实体,它不仅仅是实体,还是实体的管理者。
- 作用:
① 避免
由于复杂数据模型缺少统一的业务规则控制,而导致聚合、实
体之间数据不一致性
的问题。
② 作为实体
,具备自己的业务属性,业务行为,业务逻辑。
③ 作为聚合的管理者
, 在聚合内部,负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。
④ 聚合之间,它还是聚合对外的接口人
,以聚合根 ID 关联的方式接受外部任务和请求,在上下文内实现聚合之间的业务协同。也就是说,聚合之间通过聚合根 ID 关联引用,如果需要访问其它聚合的实体,就要先访问聚合根,再导航到聚合内部实体,外部对象不能直接访问聚合内实体。
- 特点:
聚合根是实体,有实体的特点,具有全局唯一标识,有独立的生命周期。一个聚合只有一个聚合根,聚合根在聚合内对实体和值对象采用直接对象引用的方式进行组织和协调,聚合根与聚合根之间通过 ID 关联的方式实现聚合之间的协同。
领域事件(Domain Event)
概念
领域事件是解耦微服务的关键,也是领域模型中非常重要的一部分,用来表示领域中发生的事件。一个领域事将导致进一步的业务操作,在实现业务解耦的同时, 还有助于形成完整的业务闭环。
领域模型
领域模型分为4大类:失血模型、贫血模型、充血模型、胀血模型。这类理论都是由软件设计领域的大牛(如Martin Fowler)提出来的,有其背景和原因。
失血模型
Domain Object(领域对象)模型仅仅包含对象属性的定义和操作对象属性的getter/setter方法,所有的业务逻辑完全由Business Logic层(业务逻辑层)中的服务类来完成。这种类在java中叫POJO,在.NET中叫POCO。
贫血模型
Domain Object(领域对象)模型包含对象属性的定义和操作对象属性的getter/setter方法并包含了对象的行为(例如:就像一个完整的人,具有一些属性如姓名、性别、年龄等,还具有一些能力,如走路、吃饭、恋爱等,这样才是一个完整的对象), 但不包含依赖Dao层(持久层)的业务逻辑。这部分依赖于Dao层的业务逻辑将会放到Business Logic层(业务逻辑层)中的服务类来实现,组合逻辑也由服务类负责。可以看出,贫血模型中的领域对象是不依赖于持久层的。代码架构层次结构是: Client-> Business Facade Service -> Business Logic Service(Business Logic Service是依赖Domain Object的行为) -> Data Access Service
充血模型
Domain Object(领域对象)模型包含对象属性的定义和操作对象属性的getter/setter方法并包含了大多数相关的业务逻辑,也包含了依赖于持久层的业务逻辑, Business Logic层是很薄的一层,仅仅简单封装少量业务逻辑以及控制事务、权限逻辑等,不和DAO层打交道。所以,使用充血模型的领域对象是依赖于持久层的。代码架构层次结构是: Client-> Business Facade Service -> Business Logic Service -> Domain Object -> Data Access Service
胀血模型
Domain Object(领域对象)模型包含对象属性的定义和操作对象属性的getter/setter方法并包含了所有相关的的业务逻辑,也包含了不相关的其它应用逻辑(如授权、事务等)。胀血模型取消了Business Logic层(业务逻辑层),只剩下Domain Object和DAO两层,在Domain Object的Domain Logic上面封装事务,授权逻辑等。
在面向对象设计中有一种设计原则叫做依赖导致原则( Dependence Inversion Principle,DIP)。
DIP的定义为:
高层模块不应该依赖于底层模块,二者都应该依赖于抽象。
抽象不应该依赖于细节,细节应该依赖于抽象。