写在前面:
- 你好,欢迎你的阅读!
- 我热爱技术,热爱分享,热爱生活, 我始终相信:技术是开源的,知识是共享的!
- 博客里面的内容大部分均为原创,是自己日常的学习记录和总结,便于自己在后面的时间里回顾,当然也是希望可以分享自己的知识。目前的内容几乎是基础知识和技术入门,如果你觉得还可以的话不妨关注一下,我们共同进步!
- 除了分享博客之外,也喜欢看书,写一点日常杂文和心情分享,如果你感兴趣,也可以关注关注!
- 微信公众号:傲骄鹿先生
领域驱动设计(Domain-Driven Design,DDD )是由Eric Evans最先提出,目的是对软件所涉及到的领域进行建模,以应对系统规模过大时引起的软件复杂性的问题。整个过程大概是这样的,开发团队和领域专家一起通过 通用语言(Ubiquitous Language)去理解和消化领域知识,从领域知识中提取和划分为一个一个的子领域(核心子域,通用子域,支撑子域),并在子领域上建立模型,再重复以上步骤,这样周而复始,构建出一套符合当前领域的模型。与现在的分布式、微服务相比,绝对是即将步入中年的“老家伙”了。直到近些年微服务理论被提出、被互联网行业广泛使用,人们似乎又重新发现了领域驱动设计的价值。所以看起来也确实是因为微服务,领域驱动设计才迎来了第二春。
什么是DDD,“DDD是一种可以借鉴的思想,而非严格遵循的方法论”。
学习领域驱动设计的意义在于:
1、什么是领域/子领域(Domain/Subdomain)
领域是与某个特定问题相关的知识和行为。比如支付平台就属于特定的领域,只要是这个领域,都会有账户、会记、收款、付款、风控等核心环节。所以,同一个领域的系统都具有相同的核心业务,他们要解决的问题的本质是一致的。一个领域本质上可以理解为就是一个问题域,只要是同一个领域,那问题域就相同。所以,只要我们确定了系统所属的领域,那这个系统的核心业务,即要解决的关键问题、问题的范围边界就基本确定了。
在日常开发中,我们通常会将一个大型的软件系统拆分成若干个子系统。这种划分有可能是基于架构方面的考虑,也有可能是基于基础设施的。在DDD中,我们对系统的划分是基于领域(基于业务)的。比如上文提到支付平台是一个领域,而账户、会记、收款、付款等则为子领域。一个领域由众多子领域聚集而形成。
在一个领域/子域中,我们会创建一个概念上的领域边界,在这个边界中,任何领域对象都只表示特定于该边界内部的确切含义。这样的边界便称为限界上下文。限界上下文和领域具有一对一的关系。从物理层面讲,一个限界上下文最终可以是一个Jar/War文件,甚至可以是一个Package中的所有对象。但是,技术本身并不是用来界分限界上下文。
2、设计(Design)和驱动(Driven)
DDD中的设计主要指领域模型的设计。DDD是一种基于模型驱动开发的软件开发思想,强调领域模型是整个系统的核心,领域模型也是整个平台的核心价值。每一个领域都有一个对应的领域模型,领域模型能够很好的解决负责的业务问题。所以领域模型的设计和架构设计同等重要。
DDD中,总是以领域为边界,分析领域中的核心问题(核心关注点)。然后设计对应的领域模型,通过领域模型驱动代码的实现。而数据库设计、持久化技术这些都不是DDD的核心,属于外围的东西。与数据库驱动开发的思路形成对比,驱动中需要记住两个原则:领域驱动领域模型设计,领域模型驱动代码实现。
3、领域驱动设计中的领域模型
回想日常的开发过程,日常建表,然后写CRUD,因此也有一句很真实的话“面试造火箭,工作拧螺丝”。其根本原因在于表驱动思想,而不是领域驱动设计。
前者只能增加数据库的表数量,而后者才会形成长期的、具有业务意义的模型,这样的系统生命力才更加长久。我们也才能用工程的方法来编码,从编码转身为业务域的开发专家。有很多关于领域驱动设计的论述中都并未明确我们如何得到“领域”,只有合理的领域模型才能有效驱动设计开发。所以建好领域模型是关键,对于领域模型的思考与技术框架升级同样重要。
4、从分层架构到六边形架构
4.1 分层架构
分层架构是运用最为广泛的架构模式,几乎每个软件系统都需要通过层来隔离不同的关注点,以此应对不同需求的变化,使得这种变化可以独立进行;各个层、甚至同一层中的各个组件都会以不同速率发生变化。
这里所谓的“以不同速率发生变化”,其实就是引起变化的原因各有不同,这正好是单一职责原则(Single-Responsibility Principle,SRP)的体现。即“一个类应该只有一个引起它变化的原因”,换言之,如果有两个引起类变化的原因,就需要分离。
单一职责原则可以理解为架构原则,这时要考虑的就不是类,而是层次。例如网络七层协议是一个定义的非常好的、经典的分层架构,简单、易于学习理解,最终被广泛使用进而大大推动了网络通信的发展。
通常情况下,我们会把软件系统分为这几个层:UI界面(或者接入层)、应用独有的业务逻辑、领域普适的业务逻辑、数据库等。
接下来,还有什么不同原因的变更呢?答案正是这些业务逻辑本身!在每一层内部,不同的业务场景发生变化的原因、频次也都不同,不同的场景我们分别定义为业务用例。由此,我们可以总结出一个模式:在将系统水平切分成多个分层的同时,按用例将其切分成多个垂直切片。这样做的好处就是对单个用例的修改并不会影响其他用例。
如果我们同时对支持这些用例的UI和数据库也进行了分组,那么每个用例使用各自的UI表现与数据库,这样就做到了自上而下的解耦。另一方面,有层次就有依赖。在OSI协议中,上层透明的依赖下层。但是在软件架构中,我们更强调“依赖抽象”。即组件A依赖B的功能,我们的做法是在A中定义其需要用到的接口,由B去实现对应接口能力,这样就做到了可插拔,将来我们可以把B替换为同样实现了接口能力的组件C而对系统不会造成影响。
4.2 整洁架构
分层架构中给人的感觉是每一层都同样重要,但如果我们把关注的重点放在领域层,同时把依赖关系按照业务由重到轻形成一个以领域层为中心的环,即演变为一种整洁的架构风格。这里不是说其他层不重要,仅仅是为了凸显承载了业务核心的领域能力。
整洁架构最主要原则是依赖原则,它定义了各层的依赖关系,越往里,依赖越低,代码级别越高。外圆代码依赖只能指向内圆,内圆不知道外圆的任何事情。一般来说,外圆的声明(包括方法、类、变量)不能被内圆引用。同样的,外圆使用的数据格式也不能被内圆使用。
整洁架构各层主要职能如下:
Entities:实现领域内核心业务逻辑,它封装了企业级的业务规则。一个 Entity 可以是一个带方法的对象,也可以是一个数据结构和方法集合。一般我们建议创建充血模型。
Use Cases:实现与用户操作相关的服务组合与编排,它包含了应用特有的业务规则,封装和实现了系统的所有用例。
Interface Adapters:它把适用于 Use Cases 和 entities 的数据转换为适用于外部服务的格式,或把外部的数据格式转换为适用于 Use Casess 和 entities 的格式。
Frameworks and Drivers:这是实现所有前端业务细节的地方,UI,Tools,Frameworks 等以及数据库等基础设施。
4.3 六边形架构
我们把整洁架构的外部依赖按照其输入输出功能、资源类型进行整合。将存储、中间件、与其他系统的集成、http调用分别暴露一个端口。则会演变成下面的架构图。
系统能平等地被用户、其他程序、自动化测试或脚本驱动,也可以独立于其最终的运行时设备和数据库进行开发和测试”这是六边形的精髓。
该架构由端口和适配器组成,所谓端口是应用的入口和出口,在许多语言中,它以接口的形式存在。例如以取消订单为例,“发送订单取消通知”可以被认为是一个出口端口,订单取消的业务逻辑决定了何时调用该端口,订单信息决定了端口的输入,而端口为上游的订单相关业务屏蔽了其实现细节。
而适配器分为两种,主适配器(别名Driving Adapter)代表用户如何使用应用,从技术上来说,它们接收用户输入,调用端口并返回输出。Rest API是目前最常见的应用使用方式,以取消订单为例,该适配器实现Rest API的Endpoint,并调用入口端口OrderService,当然service内部可能发送OrderCancelled事件。同一个端口可能被多种适配器调用,本场景的取消订单也可能会被实现消息协议的Driving Adapter调用以便异步取消订单。
次适配器(别名Driven Adapter)实现应用的出口端口,向外部工具执行操作,例如向MySQL执行SQL,存储订单;使用Elasticsearch的API搜索产品;使用邮件/短信发送订单取消通知。有别于传统的分层形象,形成一个六边形,因此也会称作六边形架构。
1、界限上下文
领域中还同时存在问题空间(problem space)和解决方案空间(solution space)。在问题空间中,我们思考的是业务所面临的挑战,而在解决方案空间中,我们思考如何实现软件以解决这些业务挑战。
通常,我们希望将子域一对一地对应到限界上下文。这种做法显式地将领域模型分离到不同的业务板块中,并将问题空间和解决方案空间融合在一起。
但是在实践中,这种做法并不总是可能的,想像一下,谁没有维护过“毛线团”系统,现在我们就要借助界限上下文来安全的、合理的、快速的理顺这堆交织不清的关系。
电子商务系统是个典型的“大线团”,我们按照经验将其在逻辑上拆解为:产品目录子域、订单子域、发票子域,当然你也可以拆解出更多的子域,甚至将产品目录子域继续向下分解为类目子域、商品子域(虚线是逻辑子域)。另外还有一个专门用于库存管理的库存系统、以及用于销售预测的预测系统。
电商系统里面也存在物流相关的业务逻辑,同时物流又不可避免的作用于库存逻辑之上。而往往最难以把握的就是这部分相交的地方,这才是实际的项目场景,我们通常做法是将其归并为一个新的履约系统,作为一个支撑子域去辅助主要的电商系统。
当然,随着业务不断发展,我们的履约模式(比如支持同城当日达、商家仓储发货、电商集货仓发货、退货等等)、库存类型(调拨库存、越库操作、临期库存、残次库存等等)越来越复杂,我们考虑将其再向下分解为履约系统2.0、库存系统2.0。
核心就是我们可以在概念上使用多个子域来分解较大的界限上下文,也可以将多个分散的界限上下文包含在同一个新的子域当中,最终做到“子域和界限上下文一一对应”。我个人觉得,这个过程是最考验内功心法的地方。
上面我们已经说了会拆解出来新的子域,目的使“整洁干净”的界限上下文能够一对一的解决这个子域对应的问题空间,但是随着拆解就必然导致“关联关系”。因为要解决问题空间,必须使用对应的子域,你可以把它拆解出去,但是它始终存在于依赖网中。
我们通用的做法是在相交的地方,定义接口。由支撑的界限上下文去实现,可以做到支撑上下文的插拔式切换。这里仍然是我们强调的“依赖抽象”“解耦”。
2、Repository
“对于每种需要进行全局访问的对象,我们都应该创建另一个对象来作为这些对象的提供方,就像是在内存中访问这些对象的集合一样。为这些对象创建一个全局接口以供客户端访问。为这些对象创建添加和删除方法……
此外,我们还应该提供能够按照某种指定条件来查询这些对象的方法……只为聚合创建资源库”引用自《领域驱动设计》。大家和我的疑问一样,Repository是什么?DAO与Repository什么区别?为什么需要Repository?
首先,Repository 是一个独立的层,介于领域层与数据映射层(数据访问层)之间。
它的存在让领域层感觉不到数据访问层的存在,它提供一个类似集合的接口提供给领域层进行领域对象的访问。Repository 是仓库管理员,领域层需要什么东西只需告诉仓库管理员,由仓库管理员把东西拿给它,并不需要知道东西实际放在哪。其核心还是“解耦”,所以我们应该明确领域层只应该使用Repository获取对象。
接下来,看看DAO与Repository什么区别。
我的理解是这样,你可以将Repository当作 DAO 来看待,但是请注意一点,在设计Repository时,我们应该采用面向集合的方式,而不是面向数据访问的方式。这有助于你将自己的领域当作模型来看待,而不是 CRUD 操作;Repository是面向领域的,Repository定义的目的不是DB驱动的,Repository管理的数据的最小粒度是聚合根,这两点和DAO有很大不同。
通常我们建议把Repository定义为一个集合并且只提供类似集合的接口,比如Add,Remove,Get这种操作。一言以蔽之,我们要用集合的思想来操作聚合根,而不是传统的面向DB的CRUD方法。
最后来看看为什么需要Repository,我理解还是“解耦”。当我们把Repository想象成一个资源库,也不关心背后的持久化,这些也不是DDD该思考的东西,我们可以用mysql来实现,也可以用mongo,甚至redis。尤其是当我们在更换底层存储时候,领域层以及相关的服务并无任何影响。
那么微服务和DDD是什么关系呢?其实在2015年的一次演讲中,DDD的提出者Eric Evans表达了对微服务技术的热爱与支持,认为微服务是让DDD落地的好工具。因为DDD和微服务其本质是降低软件项目的复杂性,而DDD是一种设计理念/设计方法,DDD需要有强制性的原则做保障,否则不同的领域对象终究会混在一起。而微服务本身的一些限制,以及大家都能理解微服务的实施前提和首要条件,会在实现上给DDD增加了一些原则限制。DDD和微服务的不一定要同时使用落地,但是如果将DDD和微服务结合一起,效果是非常不错的。