DDD-经典四层架构应用

DDD分层与传统三层区别

根据DDD领域驱动设计原则,对应的软件架构也需要做出相应的调整。
我们常用的三层架构模型划分为表现层,业务逻辑层,数据访问层等,在DDD分层结构中既有联系又有区别,
个人认为主要有如下异同:

  • 在架构设计上,在DDD分层结构中将传统三层架构的业务逻辑层拆解为应用层和领域层
    其中Application划分为很薄的一层服务,非核心的逻辑放到此层去实现,核心的业务逻辑表现下沉到领域层去实现,凝练为更为精确的业务规则集合,通过领域对象去阐述说明。
    与传统三层区别

  • 在建模方式上,DDD分层的建模思维方式有别于传统三层
    传统三层通常是以数据库为起点进行数据库分析设计,而DDD则需要以业务领域模型为核心建模(即面向对象建模方式),更能体现对现实世界的抽象。
    在DDD分层凸显领域层的重要作用,领域层为系统的核心,包括所有的业务领域模型的抽象表达

  • 在职责划分上,基础设施层涵盖了2方面内容

    • 持久化功能,其中原三层架构的数据访问层下沉到基础设施层的持久化机制实现
    • 通用技术支持,一些公共通用技术支持也放到基础设施层去实现。

DDD分层详解

四层架构图

在这里插入图片描述

在该架构中,上层模块可以调用下层模块,反之不行。即

  • Interface ——> application | domain | infrastructure
  • application ——> domain | infrastructure
  • domain ——> infrastructure

分层作用

分层英文描述
表现层User Interface用户界面层,或者表现层,负责向用户显示解释用户命令
应用层Application Layer定义软件要完成的任务,并且指挥协调领域对象进行不同的操作。该层不包含业务领域知识。
领域层Domain Layer或称为模型层,系统的核心,负责表达业务概念,业务状态信息以及业务规则。即包含了该领域(问题域)所有复杂的业务知识抽象和规则定义。该层主要精力要放在领域对象分析上,可以从实体,值对象,聚合(聚合根),领域服务,领域事件,仓储,工厂等方面入手
基础设施层 Infrastructure Layer主要有2方面内容,一是为领域模型提供持久化机制,当软件需要持久化能力时候才需要进行规划;一是对其他层提供通用的技术支持能力,如消息通信,通用工具,配置等的实现;

领域对象

根据战术设计,关注的领域对象主要包括有

类型英文描述
值对象 value object 无唯一标识的简单对象
实体entity充血的领域模型,有唯一标识
聚合(聚合根)aggregate实体的聚合,拥有聚合根,可为某一个实体
领域服务service无法归类到某个具体领域模型的行为
领域事件event 不常用
仓储repository持久化相关,与基础设施层关联
工厂factory负责复杂对象创建
模块module子模块引入,可以理解为子域划分

DDD编码实践(改进分层)

本文在对上述的传统四层的实践中,(1)根据依赖倒置原则对分层结构进行了改进,通过改变不同层的依赖关系(即将基础设施层倒置)来改进具体实现与抽象之间关系;(2)在基础设施层中增加引用适配层(防腐层)来增强防御策略,用来统一封装外部系统接口的引用。改进的分层结构如下:
在这里插入图片描述

依赖倒置原则(DIP):

  • 高层模块不依赖于低层模块,两者都依赖于抽象;
  • 抽象不应该依赖于细节,细节应依赖抽象

代码结构描述

eg.后端Java代码工程为例,
表现层在此代码结构中表现为api层,对外暴露接口的最上层

├─com.company.microservice
├─com.company.microservice
│    │ 
│    ├─apis   API接口层 
│    │    └─controller       控制器,对外提供(Restful)接口
│    │ 
│    ├─application           应用层
│    │    ├─model            数据传输对象模型及其装配器(含校验)
│    │    │    ├─assembler   装配器,,实现模型转换eg. apiModel<=> domainModel
│    │    │    └─dto         模型定义(含校验规则)      
│    │    ├─service          应用服务,非核心服务,跨领域的协作、复杂分页查询等
│    │    ├─task             任务定义,协调领域模型
│    │    ├─listener         事件监听定义
│    │    └─***              others
│    │ 
│    ├─domain   领域层
│    │    ├─common           模块0-公共代码抽取,限于领域层有效  
│    │    ├─module-xxx       模块1-xxx,领域划分的模块,可理解为子域划分     
│    │    ├─module-user      模块2-用户子域(领域划分的模块,可理解为子域划分)
│    │    │    ├─action      行为定义
│    │    │    │    ├─UserDomainService.java        领域服务,用户领域服务
│    │    │    │    ├─UserPermissionChecker.java    其他行为,用户权限检查器
│    │    │    │    ├─WhenUserCreatedEventPublisher.java     领域事件,当用户创建完成时的事件 
│    │    │    ├─model       领域聚合内模型 
│    │    │    │    ├─UserEntity.java                领域实体,有唯一标识的充血模型,如本身的CRUD操作在此处
│    │    │    │    ├─UserDictVObj.java              领域值对象,用户字典kv定义       
│    │    │    |    ├─UserDPO.java                   领域负载对象    
│    │    │    ├─repostiory  领域仓储接口
│    │    │    │    ├─UserRepository.java
│    │    │    ├─reference   领域适配接口
│    │    │    │    ├─UserEmailSenderFacade.java
│    │    │    └─factory     领域工厂  
│    │ 
│    ├─infrastructure  基础设施层
│    │    ├─persistence      持久化机制
│    │    │    ├─converter   持久化模型转换器
│    │    │    ├─po          持久化对象定义 
│    │    │    └─repository.impl  仓储类,持久化接口&实现,可与ORM映射框架结合
│    │    ├─general          通用技术支持,向其他层输出通用服务
│    │    │    ├─config      配置类
│    │    │    ├─toolkit     工具类  
│    │    │    ├─extension   扩展定义  
│    │    │    └─common      基础公共模块等 
│    │    ├─reference        引用层,包装外部接口用,防止穿插到Domain层腐化领域模型等
│    │    │    ├─dto         传输模型定义
│    │    │    ├─converter   传输模型转换器       
│    │    │    └─facade.impl 适配器具体实现,此处的RPC、Http等调用
│    │ 
│    └─resources  
│        ├─statics  静态资源
│        ├─template 系统页面 
│        └─application.yml   全局配置文件

其中在上述目录结构中,Domain层中为对module进行划分,实际上默认该层只有一个模块,根据微服务划分可以进行增加模块来规范代码结构。
示例代码工程:

GITHUB地址:https://github.com/smingjie/bbq-ddd.git

扩展定义注解和接口声明

(1)自定义注解:在使用DDD中自定义了标记的注解( @DDDAnnotation)和其衍生子注解,分别是

  • @DomainAggregate
  • @DomainAggregateRoot
  • @DomainEntity
  • @DomainValueObject
  • @DomainService
  • @DomainRepository
  • @DomainEvent
  • @ApplicationService
  • @DomainAssembler
  • @DomainConverter

等注解,详见代码的infrastructure.general.extension.ddd.annotation.**;其中有些注解继承了spring的 @Component,将会自动注册为spring bean,有些注解为了标记用于后续扩展;

引入了 Assembler装配器/Converter转换器,通过组合模式解耦继承关系,在api层和持久化层都有相应的实现。

(2)自定义接口:在domain.common定义了部分通用的契约接口,如领域对象元数据获取接口 IDomainMetaData,通过接口解耦继承关系。其他还有: IDomainSaveOrUpdate IDomainDelete ... 等Command

领域模型注入仓储类的问题

区别于传统的分层后,在domain中更多关注业务逻辑,考虑到要与spring框架集成,需要注意一个领域模型中注入仓储类的问题

在传统分层中,controller,service,repo均注册为spring管理的bean,
但是在domain层中,service一部分的业务逻辑划分到了具体的领域对象中去实现了,显然这些对象却不能注册为单例bean,
因此在此处不能沿用与原来分层结构中service层中通过@Autowired or @Resource等注入仓储接口,

关于这个问题,此处建议使用ApplicationContext实现

即通过一个工具类 ApplicationContextUtils 实现 ApplicationContextAware获取bean的方法,即 getBean()方法,
然后我们就可以在我们的领域模型中直接应用该工具类来获取Spring托管的singleton对象,即
xxxRepo=ApplicationContextUtils.getBean(“xxxRepository”)

@Component
public class ApplicationContextUtils implements ApplicationContextAware {

    public static ApplicationContext appctx;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        ApplicationContextUtil.appctx=applicationContext;
    } 

    /**
     * @return ApplicationContext
     */
    public static ApplicationContext getApplicationContext() {
        return appctx;
    }

    /**
     * 获取对象
     *
     * @param name spring配置文件中配置的bean名或注解的名称
     * @return 一个以所给名字注册的bean的实例
     * @throws BeansException 抛出spring异常
     */ 
    public static <T> T getBean(String name) throws BeansException {
        return (T) appctx.getBean(name);
    }

    /**
     * 获取类型为requiredType的对象
     *
     * @param clazz 需要获取的bean的类型
     * @return 该类型的一个在ioc容器中的bean
     * @throws BeansException 抛出spring异常
     */
    public static <T> T getBean(Class<T> clazz) throws BeansException {
        return appctx.getBean(clazz);
    }

    /**
     * 如果ioc容器中包含一个与所给名称匹配的bean定义,则返回true否则返回false
     *
     * @param name ioc容器中注册的bean名称
     * @return 存在返回true否则返回false
     */
    public static boolean containsBean(String name) {
        return appctx.containsBean(name);
    }
}

考虑到代码结构简洁性,还可以封装一层仓储工厂,只用来获取相应的仓储Bean

/**
 * 简化版的仓储工厂--用来统一获取仓储的实现Bean
 *
 * @author jockeys
 * @date 2020/9/12
 */
public class RepoFactory {

	/**
	 * 根据仓储接口类型获取对应实现且默认取值第一个
	 *
	 * @param tClass 具体目标类型
	 * @param <T>    仓储接口类型
	 * @return 如果不是指定实现,默认获得第一个实现Bean
	 */
	public static <T> T get(Class<? extends T> tClass) {

		Map<String, ? extends T> map = ApplicationUtils.getApplicationContext().getBeansOfType(tClass);
		Collection<? extends T> collection = map.values();
		if (collection.isEmpty()) {
			throw new PersistException("未找到仓储接口或其指定的实现:" + tClass.getSimpleName() );
		}
		return collection.stream().findFirst().get();
	}
}

然后在领域模型中就可以直接调用该工厂方法来获取仓储接口的实现,
比如DictRepo为定义的仓储接口,DictDao为该接口的准实现类

//直接指定实现
DictRepo repo= RepoFactory.get(DictDao.class);
//不指定实现取Spring容器中默认注册第1位的Bean
DictRepo repo= RepoFactory.get(DictRepo.class);

一些个人思考…

上述经典四层架构,笔者更愿意理解为DDD在编码实现阶段的一个体现或应用。

补充一点:DDD除了在编码实践阶段,还体现在需求分析、设计阶段等过程,DDD推荐不割裂系统的需求和设计,我们这里可以合并称作系统建模过程,可参考 DDD-建模过程分析一文,不再赘述。

当然除了这个经典四层架构模型,DDD还有五层架构、六边形架构等,所以这里抛出一个问题,

项目按上述经典四层架构进行搭建,可以说是DDD架构实践么?

关于这个问题,笔者想引入一对哲学概念,哲学有言形式与内容,现象与本质等辩证关系(当然与本文可能也没啥太大关系啦);从这两个角度来阐述本人的观点:

  • 形式与内容:经典四层架构是一个DDD实现的形式,相当于给我们提供了一个框框来让我们自己去实现;在这个框框里面我们怎么实现是自由发挥的,但也是有约束的,这个约束体现在DDD对每一层的作用的约定,如每个层约定做了什么功能,充当什么角色等。尤其是对Domain层的约定,才是最重要的。那么我们按照哲学辩证的套话来说,形式上满足了DDD架构,但这应该是片面的,具体还要看内容,即具体实现是怎样的。
  • 现象与本质:接着上述观点,如果要看实现,就要具体分析一下现象与本质嘞。上面笔者也有提到,DDD除了四层经典架构,还有五层架构(包括其演化的多层架构)、六边形架构等也都是DDD提供的架构模型(形式),那这些都可以理解DDD架构模式的外显形式,那么又有哪些共性呢?可自行查询,本文直接给结论,即它们都有Domain层,Domain层,Domain层(重要的事情说三遍~~,该结论DDD作者译著有写到…),所以不管架构模式怎么演化,Domain是核心不能变。
    那么如上分析,我们在回到这个问题,我们是不是可以给出一个这样的答案:

    形式上符合DDD架构,具体是不是DDD的架构实践,本质上还要看

    • (1)项目是否包括有Domain层;
    • (2)Domain层是否满足DDD战术篇的要求(或者可暂时简单理解为充血模型吧)

题外话:Spring与DDD

  • Spring框架中,Spring为我们提供了@Service @Repository 等注解,为我们分离行为和行为(注册为Bean)和属性(数据模型),同时通过@Autowired在合适地方进行注入行为,因为行为被注册为Spring容器中的Bean后,减少了频繁创建行为的开销,只有属性的数据模型作为数据的载体来传递数据。提供很大的便捷性。但也阻碍了我们应用DDD编码实践, Spring框架主张分离,DDD思想主张合并,我们在Spring框架中使用DDD则需要在其基础上进行一些权衡取舍,即 如何将注册为Bean的行为穿插到原有的贫血模型中来构建充血模型是我们要解决的问题
  • 关于这个问题,笔者使用了Spring框架提供的获取容器内已经注册的Bean接口,直接调用接口,在有属性的领域模型中来获取行为;主要还是体现融入领域模型中的部分Service获取仓储接口来实现持久化过程。

当然,上述的说明都是从一个软件开发人员的角度来阐述说明DDD在编码实践阶段的应用 。
除此之外在业务领域的建模分析过程中也可引入该概念。
比如我们现在所倡导的微服务化,如何划分或拆分微服务;如何有效地区分限界上下文,划分子域;如何构建一个有效的聚合,识别聚合根等。。。

  • 47
    点赞
  • 257
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
### 回答1: DDD设计是一种以领域(Domain)为核心进行软件开发的方法论,它强调将业务逻辑与技术实现分离,将复杂的业务领域进行划分,提高代码可维护性和可扩展性。对于代码目录结构来说,它应该贴近领域,分层清晰明确,具有可读性和可维护性。以下是一个典型的DDD设计代码目录结构: 1. Application层:作为上层代码,包含应用程序的核心逻辑和业务流程。它与领域模型紧密耦合,负责传递外部请求给领域层进行处理。 2. Domain层:包含业务领域模型,它是整个系统的核心,负责处理业务逻辑和持久化数据。该层包含实体(Entity)、值对象(Value Object)、聚合(Aggregate)、工厂(Factory)、仓储(Repository)等领域模型元素。 3. Infrastructure层:为领域层提供支持和依赖,负责与外部基础设施(如数据库、缓存、日志、消息队列等)进行交互。该层使用一些开源框架和库实现技术实现。 4. Interface层:为外部应用提供服务的接口,它包含Web API、消息MQ、命令行CLI等。该层只负责接收请求和返回响应,没有具体业务逻辑和数据操作。 另外,对于DDD设计来说,最重要的是领域模型,设计好领域模型是代码目录结构的基础,其次是业务逻辑分层清晰,职责分明,分离关注点,降低代码复杂性。代码目录结构应该根据实际需求进行调整,可以遵循DDD的规范,也可以自定义一些目录结构。最终目标是使代码的维护成本更低,提高代码质量和开发效率。 ### 回答2: DDD(领域驱动设计)是一种软件开发设计思想,它注重领域模型的设计以及实现,能够有效地减少软件开发过程中的复杂性和不确定性。在DDD中,代码目录结构应该与实现的模型和领域架构相匹配,以确保模型的可维护性和代码的可读性。最常见的代码目录结构如下: 1.应用程序:应用程序处理业务逻辑,是与用户交互的入口。在应用程序中,通常有控制器、命令或事件处理程序等。这些应该按照模块或功能对其进行结构化组织。 2.领域层:实现领域模型,是相对独立的。在实现领域模型时,可以将其分组到例如聚合根、实体、值对象、仓库、服务等目录下。 3.基础设施:这一层包含与基础设施相关的实现,比如说持久性、第三方库、工具等。基础设施应该像一个插件那样工作,不应该改变领域层或应用程序设计。 4.界面层:显示用户界面以及处理用户输入,连接应用程序与实际用户。界面层通常有几个子目录,例如视图、控制器、资源等。 总的来说,DDD的目录结构应该先设计好领域模型,在此基础上组织代码和目录。这可以确保代码的复用性、可扩展性,并且使得代码更具有可读性、协同性等。 ### 回答3: DDD设计(领域驱动设计)是一种软件开发的方法论,主要强调对领域进行高度抽象与模型化。在进行软件开发时,良好的代码目录结构能够更好地组织和管理代码,提高代码的可读性和可维护性。 DDD的代码目录结构一般可以分为三层:应用层、领域层和基础设施层。 应用层:主要负责应用程序的生命周期和交互,包括用户界面、任务调度、服务间通信等。应用层应该只是一些简单的委托工作,具体的业务逻辑应该放在领域层。 领域层:这一层是DDD设计最核心的部分,需要对领域的核心问题进行建模,提供相关的领域服务和领域模型。重点在于对业务逻辑和领域模型的设计和实现,需要进行充分的领域建模和领域分析。 基础设施层:基础设施层主要提供对第三方库和框架的封装,以及对数据库、缓存、日志等底层服务的提供和管理。这一层不应该直接与领域相关。 总之,DDD的代码目录结构应该从领域建模和业务逻辑的设计出发,充分实现领域驱动设计的思想,同时兼顾代码的可维护性和可读性。
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值