领域驱动设计(DDD)学习笔记
DDD简介
我们先来看看Wikipedia对DDD的解释:
领域驱动设计(Domain-Driven Design,DDD)是一种通过将实现连接到持续进化的模型来满足复杂需求的软件开发方法。领域驱动设计的前提是:
1. 把项目的主要重点放在核心领域(Core Domain)上
2. 把复杂的设计放在有界域(Bounded Context)模型上
3. 发起一个创造性的合作之间的技术,和领域专家以迭代的方式完善领域模型,解决特定领域的问题
领域驱动设计不是用来解决开发速度的,领域驱动设计是用来解决软件核心复杂性的。应用场景是行为中心(供应链)的软件系统,而不是数据中心(微博)的软件系统。DDD提供了一个组织应用程序和构建代码的思考方式。
领域模型:显示最重要的业务概念和它们之间的关系的类图,是没有方法的类图的集合。领域模型的特点如下:
面向对象分析和设计
领域模型具备自己的属性行为状态,并与现实世界的业务对象相映射
各类具备明确的职责划分,领域对象元素之间通过聚合和引用等关系配合解决实际业务应用和规划
可复用,可维护,易扩展
可以采用合适的设计模型进行详细设计
要求设计人员有良好的抽象能力
统一语言:将模型作为语言的支柱。确保团队在内部的所有交流中以及代码中坚持使用这种语言。在画图、写东西、特别是讲话时也要使用这种语言。要认识到,Ubiquitous Language的更改就是对模型的更改。
DDD基于面向对象分析与设计技术,对技术框架进行了分层规划,同时对每个类进行了策略和类型的划分。
领域驱动设计是技术人员和领域专家沟通的桥梁。
为了更好地理解DDD,下面将从不同的角度跟与其它架构的对比来解释DDD。
DDD的分层架构
User Interface Layer:主要负责与外部服务的交互
Application Layer:定义系统的业务功能,并指挥领域层中的领域对象实现这些功能。职责主要是编排和转发,即将它要实现的功能委托给一个或多个领域对象来实现,它本身只负责安排工作顺序和拼装操作结果
Domain Layer:实现业务逻辑,主要包含以下内容:
业务实体(领域对象)
业务规则
业务策略
完整性约束
业务流程
Infrastructure Layer:为其余各层提供技术支持,也称为数据源层或数据访问层。但它不只负责数据库访问,它还实现了系统的全部技术性需求。例如通知服务等。
数据驱动 vs 领域驱动
MVC不足之处
MVC的不足之处:
没有告诉你如何设计你的领域模型,架构最后可能会混乱不堪
MVC与业务脱节,一旦业务复杂起来就可能很难维护
DDD与六边形架构(端口-适配器架构)
六边形架构是一种改进的分层架构,使用了依赖倒置原则(Dependency Inversion Principle, DIP),它通过改变不同层之间的依赖关系达到改进的目的。如下图所示,六边形架构结合了DDD,最里层的Domain Model就可以使用DDD战术建模来设计。Client只能通过Application Services来与Domain Model交互,Application Services有如下特点:
无状态
安全校验
控制DB事务
编排业务逻辑,但不做任务业务决策(即不包含任务业务逻辑)
DDD与SOA
SOA是一种粗粒度、松耦合服务架构,服务之间通过简单、精确定义接口进行通讯,不涉及底层编程接口和通讯模型。
SOA是一种架构风格,它提倡设计与业务齐合的企业服务,并将这些服务作为设计、构建、构思企业业务方案的核心单元。而DDD是一种思考方式和一组优先考虑事项,它致力于加快那些涉及复杂领域的软件项目。因此,SOA和DDD之间有着很强的互补性。我们可以以纯DDD方法开始,然后创建实现了领域对象的对象(POJOs或无状态Beans),并将它们暴露为服务。
DDD与微服务
DDD本质是一种软件设计方法,而微服务架构是具体的实现方式。微服务架构虽好,但是他并没有给出如何对复杂系统进行分解的具体方法论。而DDD正好就是解决方案。DDD强调领域模型和微服务设计的一体性,先有领域模型,然后才有微服务,而不是脱离领域模型来谈微服务设计。
DDD与微服务的关注点也不同。
微服务关注点:
运行时进程间通信,能够容错和故障隔离
去中心化管理数据和治理
服务可以独立开发、测试、构建和部署
高内聚低耦合,职责单一
DDD关注点:
关注业务领域,建立边界并构建能用语言,高效沟通
对业务进行抽象,和业务专家一起建模
尽可能维持代码和业务的低表示差异
DDD解决的问题
如何进行领域建模
如何识别Bounded Context
如何在战术层面寻找对象
因此,我们可以通过限界上下文拆分微服务:微服务-限界上下文-代码库-开发团队-进程空间。
DDD战略建模&战术建模概览
战略设计的3个基本原则:上下文
、精炼
和大型结构
战略建模
限界上下文(Bounded Context)
定义了每个模型的应用范围。以便让团队成员对什么应该保持一致以及上下文之间如何关联有一个明确和共同的理解。在Context中,要保证模型在逻辑上统一,而不用考虑它是不是适用于边界之外的情况。
明确限界上下文,首先需要识别模型概念上的不一致。模型概念上的不一致主要有以下两种情况:
重复的概念:指两个模型元素(以及伴随的实现)实际上表示同一个概念。每当这个概念的信息发生变化时,都必须更新两个地方
假同源:指使用相同术语(或已实现的对象)的两个人认为他们是在谈论同一件事情,但实际上并不是
限界上下文是项目上下文以及它们之间关系的总体视图。
每一个上下文都有它自己的统一语言跟领域模型。
持续集成(Continuous Integration)
是指把一个上下文中的所有工作足够频繁地合并到一起,并使他们保持一致,以便当模型发生分裂时,可以迅速发现并纠正问题。
主要有两个级别的集成:
模型概念的集成:团队成员之间通过经常沟通来保证概念的集成,团队必须对不断变化的模型形成一个共同的理解
实现的集成
上下文图(Context Map)
定义不同上下文之间的关系,并在项目中创建一个所有模型上下文的全局视图,以减少混乱。
康威定律:任何组织在设计一套系统(广义概念上的系统)时,所交付的设计方案在结构上都与该组织的沟通结构保持一致
共享内核(Shared Kernel)
保持整个模型和代码完全同步的开销可能太高了,但从系统中仔细挑选出一部分并保持同步,就能以较小的代价获得较大的收益。这部分明确共享的内容具有特殊的地位,一个团队在没有与另一个团队商量的情况下不应擅自更改它。
客户/供应商关系(Customer/Supplier Development Team)
在两个团队之间建立一种明确的客户/供应商关系。在计划会议中,下游团队相当于上游团队的客户。根据下游团队的需求来协商需要执行的任务并为这些任务做预算,以便每个人都知道双方的约定和进度。
两个团队共同开发自动化验收测试,用来验证预期的接口。把这些测试添加到上游团队的测试套件中,以便作为其持续集成的一部分来运行。这些测试使上游团队在作出修改时不必担心对下游团队产生副作用。
适用于两个具有上游/下游关系的团队归于同一个管理者指挥时。
跟随者(Conformist)
如果上游设计的质量不是很差,而且风格也能兼容的话,那么最好不要再开发一个对立的模型。这种情况下可以使用跟随者模式。通过严格遵从上游团队的模型,可以消除在Bouded Context之间进行转换的复杂性。尽管这会限制下游设计人员的风格,而且可能不会得到理想的应用程序模型,但选择Conformist模式可以极大地简化集成。
Conformist模式类似于Shared Kernel模式。Shared Kernel是两个高度协调的团队之间的合作模式,而Conformist模式则是应对于一个对合作不感兴趣的团队进行集成。
隔离层(Anticorruption Layer)
Anticorruption Layer是连接两个Bounded Context的一种方式。构建一个全新的层来负责两个系统之间的语义转换为我们提供了一个机会,它使我们能够重新对另一个系统的行为进行抽象,并按照与我们的模型一致的方式把服务和信息提供给我们的系统。
Service:Anticorruption Layer的公共接口通常以一组Service的形式出现,但偶尔也会采用Entity的形式
Facade:子系统的一个可替换的接口,它简化了客户访问,并使子系统更易于使用
Adapter:是一个包装器,它允许客户使用另外一种协议,这种协议可以是行为实现者不理解的协议。
独立自主模式(Separate Way)
如果两组功能之间的关系并非必不可少,那么两者完全可以彼此独立。
开放主机服务(Open Host Service)
定义一个协议,把你的子系统作为一组Service供其他系统访问。开放这个协议,以便所有需要与你的子系统集成的人都可以使用它。当有新的集成需求时,就增强并扩展这个协议,但个别团队的特殊需求除外。满足这种特殊需求的方法是使用一次性的转换器来扩充协议,以便使共享协议简单且内聚。
公共语言(Published Language)
把一个良好文档化的,能够表达出所需领域信息的共享语言作为公共的通信媒介,必要时在其他信息与该语言之间进行转换
高度合作的模式:Shared Kernel或Customer/Supplier Development Team
单方面的模式:Conformist
不合作的模式:Separate Way
战略精炼
精炼是把一堆混杂在一起的组件分开的过程,从中提取出最重要的内容,使得它更有价值,也更有用。在软件设计中,精炼就是对模型中的关键方面进行抽象,或者是对大系统进行划分,从而把核心领域提取出来。
Core Domain
对模型进行精炼。找到Core Domain并提供一种易于区分的方法把它与那些起辅助作用的模型和代码分开。最有价值和最专业的概念要轮廓分明。尽量压缩Core Domain。让最有才能的人来开发Core Domain,并据此要求进行相应的招聘。在Core Domain中努力开发能够确保实现系统蓝图的深层模型和柔性设计。
Generic Subdomain
识别出那些与项目意图无关的内聚子领域。把这些子领域的通用模型提取出来,并放到单独的Module中。
Domain Vision Statement
写一份Core Domain的简短描述(大约一页纸)以及它将会创造的价值,也就是“价值主张”。那些不能将你的领域模型与其他领域模型区分开的方面就不要写了。展示出领域模型是如何实现和均衡各方利益的。这份描述要尽量精简,尽早把它写出来,随着新的理解随时修改它。
Highlighted Core
编写一个非常简短的文档(3~7页,每页内容不必过多),用于描述Core Domain以及Core元素之间的主要交互过程。
Cohesive Mechanism(内聚机制)
把概念上的Cohesive Mechanism(内聚机制)分离到一个单独的轻量级框架中。要特别注意公式或那些有完备文档的算法。用一个Intention-Revealing Interface来暴露这个框架的功能。现在,领域中的其他元素就可以只专注于如何表达问题(做什么)了,而把解决方案的复杂细节(如何做)转移给了框架。
Core Domain或Generic Subdomain的模型描述的是事实、规则或问题;而Cohesive Mechanism则用来满足规则或者用来完成模型指定的计算。
Segregated Core
对模型进行重构,把核心概念从支持性元素(包括定义得不清楚的那些元素)中分离出来,并增强Core的内聚性,同时减少它与其他代码的耦合。把所有通用元素或支持性元素提取到其他对象中,并把这些对象放到其他的包中——即使这会把一些紧密耦合的元素分开。
Abstract Core
把模型中最基本的概念识别出来,并分离到不同的类、抽象或接口中。设计这个抽象模型,使之能够表达出重要组件之间的大部分交互。把这个完整的抽象模型放到它自己的Module中,而专用的、详细的实现类则留在由子领域定义的Module中。
大型结构(Large-Scale Structure)
一组高层的概念和或规则,它为整个系统建立了一种设计模式。它使人们能够从大的角度来讨论和理解系统
“大型结构”是一种语言,人们可以用它来从大局上讨论和理解系统。它用一组高级概念或规则来为整个系统的设计建立一种模式。这种组织原则既能指导设计,又能帮助理解设计。另外,它还能协调不同人员的工作,因为它提供了共享的整体视图,让人们知道各个部分在整体中的角色。
Evolving Order
让这种概念上的大型结构随着应用程序一起演变,甚至可以变成一种完全不同的结构风格。不要依此过分限制详细的设计和模型决策,这些决策和模型决策必须在掌握了详细知识之后才能确定。当发现一种大型结构可以明显使系统变得更清晰,而又没有对模型开发施加一些不自然的约束时,就应该采用这种结构。使用不合适的结构还不如不使用它,因此最好不要为了追求设计的完整性而勉强去使用一种结构,而应该找到尽可能精简的方式解决所出现问题。要记住宁缺毋滥的原则。
System Metaphor
软件设计往往非常抽象且难于掌握。开发人员和用户都需要一些切实可行的方式来理解系统,并共享系统的一个整体视图。系统隐喻是一种松散的、易于理解的大型结构,它与对象范式是协调的。由于系统隐喻只是对领域的一种类比,因此不同模型可以用近似的方式来与它关联,这使得人们能够在多个Bounded Context中使用系统隐喻,从而有助于协调各个Bounded Context之间的工作。
例如:网络防火墙
Responsibility Layer
注意观察模型中的概念依赖性,以及领域中不同部分的变化频率和变化的原因。如果在领域中发现了自然的层次结构,就把它们转换为宽泛的抽象职责。这些职责应该描述系统的高层目的和设计。对模型进行重构,使得每个领域对象、Aggrate和Module的职责都清晰地位于一个职责层中。
Knowledge Level
Knowledge Level是Reflection(反射)模型在领域层中的一种应用。创建一组不同的对象,用它们来描述和约束基本模型的结构和行为。把这些对象分为两个“级别”,一个是非常具体的级别,另一个级别则提供了一些可供用户或超级用户定制的规则和知识。
Pluggable Component Framework
从接口和交互中提炼出一个Abstract Core,并创建一个框架,这个框架要允许这些接口中的各种不同实现被自由替换。同样,无论是什么应用程序,只要它严格地通过Abstract Core的接口进行操作,那么就可以允许它使用这些组件。
战略评估
画出Context Map。你能画出一个一致的图吗?有没有一些模棱两可的情况?
注意项目上的语言使用。有没有Ubiquitous Language?这种语言是否足够丰富,以便帮助开发?
理解重点所在。Core Domain被识别出来了吗?有没有Domain Vision Statement?你能写一个吗?
项目所采用的技术是遵循Model-Driven-Design,还是与之相悖?
团队开发人员是否具备必要的技能?
开发人员是否了解领域知识?他们对领域是否感兴趣?
6个要点:
决策必须传达到整个团队
决策过程必须收集反馈意见
计划必须允许演变
架构团队不必把所有最好、最聪明的人员都吸收进来
战略设计需要遵守简约和谦逊的原则
对象的职责要专一,而开发人员应该是多面手
战术建模
进行战术建模,需要避免贫血模型跟充血模型:
贫血模型:是指领域对象里只有Get和Set方法(POJO),所有的业务逻辑都不包含在内而是放在Business Logic层
充血模型:是指大多业务逻辑和持久化对象放在Domain Object里面,Business Logic只是简单封闭部分业务逻辑以及控制事务、权限等
实体 (Entity)
一种对象,它不是由属性来定义的,而是通过一连串的连续事件和标识定义的。
主要由唯一标识定义的对象,具有生命周期,形式和内容可能发生根本改变,但必须保持一种内在的连续性。
值对象(Value Objects)
用于描述领域的某个方面而本身没有概念标识的对象,我们只关心它们是什么,而不关心它们是谁。
领域服务(Domain Services)
有时,对象不是一个事物,有些重要的领域操作无法放到Entity或Value Object,这个就可以用Services。
聚合(Aggregate)
一组相关对象的集合,我们把聚合作为数据修改的单元。
聚合根
外部对象只能引用聚合中的一个成员,我们把它称为聚合根。
模块(Modules)
将那些具有紧密概念关系的模型元素集中到一起。
资源库(Repository)
一种把存储、检索和搜索行为封闭起来的机制,它类似于一个对象集合。
领域事件(Domain Events)
领域事件是对领域内发生的活动进行的建模。
演进的领域驱动设计过程
DDD代码结构
参考
《领域驱动设计:软件核心复杂性应对之道》—— Eric Evans
Building Evolutionary Architectures
Domain-Driven Design and MVC Architectures
Domain Driven Design and Development In Practice
Domain-Driven Design and the Hexagonal Architecture
Microservices vs SOA: What's the Difference?
版权声明: 本文为 InfoQ 作者【Chank】的原创文章。
原文链接:【http://xie.infoq.cn/article/00da697057182b0ccfa8fd19e】。文章转载请联系作者。
评论