欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

实践DDD:逐步探索领域驱动设计的战略架构方法论

最编程 2024-02-11 08:25:51
...

需要指出的是,DDD绝非一套单纯的技术工具集,但是我所看到的很多程序员却的确是这么认为的,并且也是怀揣着这样的想法来使用DDD的。过于拘泥于技术上的实现将导致DDD-Lite。简单来讲,DDD-Lite将导致劣质的领域对象,因为我们忽略了DDD战略建模所带来的好处。DDD的战略设计主要包括领域/子域、通用语言、限界上下文和架构风格等概念。

 

领域和子域(Domain/Subdomain)

既然是领域驱动设计,那么我们主要的关注点理所当然应该放在如何设计领域模型上,以及对领域模型的划分。

领域并不是多么高深的概念,比如,一个保险公司的领域中包含了保险单、理赔和再保险等概念;一个电商网站的领域包含了产品名录、订单、发票、库存和物流的概念。这里,我主要讲讲对领域的划分,即将一个大的领域划分成若干个子域。

在日常开发中,我们通常会将一个大型的软件系统拆分成若干个子系统。这种划分有可能是基于架构方面的考虑,也有可能是基于基础设施的。但是在DDD中,我们对系统的划分是基于领域的,也即是基于业务的。

于是,问题也来了:首先,哪些概念应该建模在哪些子系统里面?我们可能会发现一个领域概念建模在子系统A中是可以的,而建模在子系统B中似乎也合乎情理。第二个问题是,各个子系统之间的应该如何集成?有人可能会说,这不简单得就像客户端调用服务端那么简单吗?问题在于,两个系统之间的集成涉及到基础设施和不同领域概念在两个系统之间的翻译,稍不注意,这些概念就会对我们精心创建好的领域模型造成污染。

如何解决?答案是:限界上下文和上下文映射图。

 

限界上下文(Bounded Context)

在一个领域/子域中,我们会创建一个概念上的领域边界,在这个边界中,任何领域对象都只表示特定于该边界内部的确切含义。这样边界便称为限界上下文。限界上下文和领域具有一对一的关系。

举个例子,同样是一本书,在出版阶段和出售阶段所表达的概念是不同的,出版阶段我们主要关注的是出版日期,字数,出版社和印刷厂等概念,而在出售阶段我们则主要关心价格,物流和发票等概念。我们应该怎么办呢,将所有这些概念放在单个Book对象中吗?这不是DDD的做法,DDD有限界上下文将这两个不同的概念区分开来。

从物理上讲,一个限界上下文最终可以是一个DLL(.NET)文件或者JAR(Java)文件,甚至可以是一个命名空间(比如Java的package)中的所有对象。但是,技术本身并不应该用来界分限界上下文。将一个限界上下文中的所有概念,包括名词、动词和形容词全部集中在一起,我们便为该限界上下文创建了一套通用语言。通用语言是一个团队所有成员交流时所使用的语言,业务分析人员、编码人员和测试人员都应该直接通过通用语言进行交流。

对于上文中提到的各个子域之间的集成问题,其实也是限界上下文之间的集成问题。在集成时,我们主要关心的是领域模型和集成手段之间的关系。比如需要与一个REST资源集成,你需要提供基础设施(比如Spring 中的RestTemplate),但是这些设施并不是你核心领域模型的一部分,你应该怎么办呢?答案是防腐层,该层负责与外部服务提供方打交道,还负责将外部概念翻译成自己的核心领域能够理解的概念。当然,防腐层只是限界上下文之间众多集成方式的一种,另外还有共享内核、开放主机服务等,具体细节请参考《实现领域驱动设计》原书。限界上下文之间的集成关系也可以理解为是领域概念在不同上下文之间的映射关系,因此,限界上下文之间的集成也称为上下文映射图。

 

架构风格(Architecture)

DDD并不要求采用特定的架构风格,因为它是对架构中立的。你可以采用传统的三层式架构,也可以采用REST架构和事件驱动架构等。但是在《实现领域驱动设计》中,作者比较推崇事件驱动架构和六边形(Hexagonal)架构。

当下,面向接口编程和依赖注入原则已经在颠覆着传统的分层架构,如果再进一步,我们便得到了六边形架构,也称为端口和适配器(Ports and Adapters)。在六边形架构中,已经不存在分层的概念,所有组件都是平等的。这主要得益于软件抽象的好处,即各个组件的之间的交互完全通过接口完成,而不是具体的实现细节。正如Robert C. Martin所说:

抽象不应该依赖于细节,细节应该依赖于抽象。

采用六边形架构的系统中存在着很多端口和适配器的组合。端口表示的是一个软件系统的输入和输出,而适配器则是对每一个端口的访问方式。比如,在一个Web应用程序中,HTTP协议可以作为一个端口,它向用户提供HTML页面并且接受用户的表单提交;而Servlet(对于Java而言)或者Spring中的Controller则是相对应于HTTP协议的适配器。再比如,要对数据进行持久化,此时的数据库系统则可看成是一个端口,而访问数据库的Driver则是相应于数据库的适配器。如果要为系统增加新的访问方式,你只需要为该访问方式添加一个相应的端口和适配器即可。

那么,我们的领域模型又如何与端口和适配器进行交互呢?

上文已经提到,软件系统的真正价值在于提供业务功能,我们会将所有的业务功能分解为若干个业务用例,每一次业务用例都表示对软件系统的一次原子操作。所以首先,软件系统中应该存在这样的组件,他们的作用即以业务用例为单位向外界暴露该系统的业务功能。在DDD中,这样的组件称为应用层(Application Layer)。

 

在有了应用层之后,软件系统和外界的交互便变成了适配器和应用层之间的交互,如上图所示。

从图中可以看出,领域模型位于应用程序的核心部分,外界与领域模型的交互都通过应用层完成,应用层是领域模型的直接客户。然而,应用层中不应该包含有业务逻辑,否则就造成了领域逻辑的泄漏,而应该是很薄的一层,主要起到协调的作用,它所做的只是将业务操作代理给我们的领域模型。同时,如果我们的业务操作有事务需求,那么对于事务的管理应该放在应用层上,因为事务也是以业务用例为单位的。

应用层虽然很薄,但却非常重要,因为软件系统的领域逻辑都是通过它暴露出去的,此时的应用层扮演了系统门面(Facade)的角色。