领域驱动,俗称DDD,即:domain-driven-design
首先,不能单纯的直接说DDD就是另外一种分层方式,这是错误的想法。
更不是什么dto转do转po,会让大家误以为不就是三层结构再加上各种实体的Mapper转换吗。
领域驱动是为了解决降低复杂的软件设计工程的一种**。注意,我说的是**,并不是代码的架构设计。
领域驱动,有两大部份:战略设计 和 战术设计。而核心**是战略设计,即告诉我们如何正确有效的使用领域驱动的**。
而战术设计通常是指我们的项目中在具体代码上是如何利用这个**来转换成编程语言代码的一种设计分层实现。
在使用战术设计之前,首先得保证你已经对项目中有了科学的领域划分、聚合划分。在这之后才是使用战术设计达到代码的实现。
要想学透领域驱动,得先掌握好战略设计部份,这个推荐大家看:《实现领域驱动设计》这本书。此书我拜读了两遍,受益匪浅。
前几年微服务非常火热,当时也一直困扰我的一个难题是:如何划分微服务,这个服务如果切分。其实如果掌握了战略设计,相信大家心中就会有答案了。
在传统计的三层架构中,数据访问层是在最底层,由上至下,依次被上层依赖。而领域驱动恰恰相反,基础设施层是作为最外层来依赖各层的。
在三层架构中,我们通常会先设计数据库表,然后再根据表来一一对应各种逻辑服务。这将导致做项目时,最优先考虑的就是数据库的设计。
这将导致一个严重的问题是,忽略了软件本身内部的复杂性。并且通常我们用三层架构时,会使用贫血模型,并允许上层任意修改实体的属性。当时间久了之后,我们将会失忆忘记,这些属性在什么背景下会被修改,因为什么修改。在后续的优化时,更不敢随意修改代码。
再比如,原来我们使用RockerMQ,后公司要求变更为Rabbit时,我们不得不对原本没有问题的代码,最终被改的到处是BUG。并且难以做单元测试。
因此,对于复杂的项目来说,使用领域驱动是非常有必要的。就算你不用,我也建议大家需要花时间去学习它。
回到领域驱动的战术部份来说,领域驱动分为四大层:接口层、应用层、领域层、基础设施层。具体各层如何依赖的以及职责,详见下文
另外,使用领域驱动的项目中,我在.NET和GO都有开源项目供大家学习。
也欢迎大家学习GO,加入到我们GO团队中一起维护GO框架。farseer-go
该层会依赖应用层,这里要注意的是,不能在此层调用领域层。所有对领域层的调用,需要通过应用层来发起。
通常我们的Web、Api、控制台、Winform就属于接口层。本身没有任何逻辑,仅是作为对外的输入、输出入口。
该层只有一个依赖,那就是:领域层。
应用层,会有应用服务。用于提供给到接口层的调用。应用层主要是解决:
1、数据库事务控制
2、1个或多个限界上下文下的聚合(或领域服务)的调用。
3、领域事件发布。
应用层不会有太多的逻辑,主要是为了控制调用的顺序,起到一个组织协调的作用。类似于设计模式的:外观层。
应用层还会定义一个实体类DTO。DTO允许被接口层直接使用。在调用聚合的时候,通常也会把DTO Mapper 成DO
开头说过,应用层只会依赖领域层。那我们需要数据库持久化方案怎么处理呢,我们可以利用IOC的依赖反转,通过IOC获取仓储接口的实例来达到持久化目的。
业务逻辑的主要实现,不依赖任何层、也不依赖技术组件(如:数据库、缓存等)
一般,一个限界上下就是一个领域,一个领域下会有:聚合(多个)、每个聚合对应一个仓储接口定义、领域事件(可以没有)、领域服务(可以没有),每个聚合内会有实体(可以没有)、值对象(可以没有)
一般来说,一个限界上下文只包含一个领域,限界上下文的划分,主要是以通用语言的维度来划分。
简单的理解为,限界上下文,即Bounded Context,是为我们的业务模块划分的一个或多个边界,边界与边界相互之间不能依赖。
限界上下文是最大的单元(一个整体),在其中,会有聚合、领域服务。
Domain Object 是一个带有唯一标识的实体类,且有自己的行为(方法函数,充血模型)。通常这个聚合命名在.NET中以DO结尾,在go中,我会直接命名:domainObject
聚合的属性字段不允许外部改变(赋值),只能由聚合内的方法来修改。(需要充分理解这个意义,好处非常多,慢慢体会)
聚合与聚合之间,不能相互依赖,且在一个事务中,只能修改一个聚合。(如果要修改多个,可以使用领域事件或消息机制)
聚合会包含仓储接口、实体、值对象、领域事件
聚合主要就是提供给外部使用的对象。且主要的逻辑都放在聚合内部。
聚合可以由:应用层、领域服务、基础设施层调用。
Entity Object 实体也是带有唯一标识的。一般我将这个实体的命名以EO结尾。与聚合的一样,也有自己的行为,只允许自己的行为修改状态。
实体是聚合的一个属性,可以是集合或是单个对象
通常在聚合中会有非常多的属性,这时我们可以将其中一些关联在一起的属性改成实体。(但注意,实体必须是包含唯一标识的)
这里要注意的是,实体的属性,不能由聚合来修改,只能是实体本身。聚合可以通过调用实体提供的行为来实现修改
Value Object 没有唯一标识。一般我将这个值对象的命名以VO结尾。
但值对象是不允许修改内部状态。如果要修改,只能整个替换。
同样,我们可以将聚合中一些关联在一起的属性改成值对象。但这些属性没有唯一标识。
仓储接口,主要就是针对数据库或缓存的持久化实现的接口定义(而具体实现是在基础设施层)
在应用层调用了聚合,并改变了行为(执行业务逻辑)后,我们始终需要将这个聚合持久化到比如数据库或缓存的,
但是聚合本身,不能直接或间接调用持久化的实现。前面也描述了,领域层是不依赖仓储的。
仓储接口一般是由:应用层、领域服务调用。聚合本身不能调用仓储接口。
常见的比如,从数据库取一条记录,然后执行聚合的行为时,这个获取记录的动作是在应用层直接调仓储接口。
领域事件主要是为了解决不同聚合之间的耦合。
如果两个聚合需要发生交互。可以由A聚合完成自身的处理时,发起一个事件通知。再由B聚合来订阅此事件,然后完成对应B聚合的行为。
因为聚合还有一个硬性要求:一次事务,只能服务于一个聚合。所以领域事件便可以通过此种方式来达到解耦。
领域服务也有类似的解决方案,但是领域事件是可以跨限界上下文的。
在同一个限界上下文中,多个聚合需要依赖时,则可以使用领域服务来解决多聚合调用问题。
但领域服务只能用在这两个聚合是在同一个限界上下文。(而领域事件可以跨限界上下文。)
这里要注意,尽可能少用领域服务。如果使用过多,则会导致贫血模型的问题,并且会像之前三层中的逻辑服务层一样泛滥,导致业务逻辑扁平化。
基础设施层会依赖:接口层、应用层、领域层。没错,基础设施层才是最外的一层。这与传统的三层架构有非常大的区别。
主要是实现技术功能,如数据库、缓存、ES、MQ,与业务逻辑无关,也包括一些领域事件的订阅、MQ的消费、JOB等。
基础设施层作为最外层的好处是,业务逻辑(领域层)不变的情况下,更换技术功能组件时,只需修改该层即可。
基础设施层会实现聚合中的仓储接口,然后通过IOC容器技术,实现注册。