欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

分布式架构设计原则:领域驱动设计与业务驱动划分

程序员文章站 2022-06-10 13:28:47
领域驱动设计(Domain-Driven Design,DDD )是由Eric Evans最先提出,目的是对软件所涉及到的领域进行建模,以应对系统规模过大时引起的软件复杂性的问题。整个过程大概是这样的,开发团队和领域专家一起通过 通用语言(Ubiquitous Language)去理解和消化领域知识,从领域知识中提取和划分为一个一个的子领域(核心子域,通用子域,支撑子域),并在子领域上建立模型,再重复以上步骤,这样周而复始,构建出一套符合当前领域的模型。...

写在前面:

  •     你好,欢迎你的阅读!
  •     我热爱技术,热爱分享,热爱生活, 我始终相信:技术是开源的,知识是共享的!
  •     博客里面的内容大部分均为原创,是自己日常的学习记录和总结,便于自己在后面的时间里回顾,当然也是希望可以分享自己的知识。目前的内容几乎是基础知识和技术入门,如果你觉得还可以的话不妨关注一下,我们共同进步!
  •     除了分享博客之外,也喜欢看书,写一点日常杂文和心情分享,如果你感兴趣,也可以关注关注!
  •     微信公众号:傲骄鹿先生

领域驱动设计(Domain-Driven Design,DDD )是由Eric Evans最先提出,目的是对软件所涉及到的领域进行建模,以应对系统规模过大时引起的软件复杂性的问题。整个过程大概是这样的,开发团队和领域专家一起通过 通用语言(Ubiquitous Language)去理解和消化领域知识,从领域知识中提取和划分为一个一个的子领域(核心子域,通用子域,支撑子域),并在子领域上建立模型,再重复以上步骤,这样周而复始,构建出一套符合当前领域的模型。与现在的分布式、微服务相比,绝对是即将步入中年的“老家伙”了。直到近些年微服务理论被提出、被互联网行业广泛使用,人们似乎又重新发现了领域驱动设计的价值。所以看起来也确实是因为微服务,领域驱动设计才迎来了第二春。

一、关于DDD

什么是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搜索产品;使用邮件/短信发送订单取消通知。有别于传统的分层形象,形成一个六边形,因此也会称作六边形架构。

二、如何DDD

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和微服务结合一起,效果是非常不错的。

本文地址:https://blog.csdn.net/cyl101816/article/details/107550494