领域驱动设计实现疑难解答(一):如何分包及组织工程结构

接到一项业务,我们首先下意识要解决这些问题,而为了解决业务问题,需要处理用户输入,处理业务逻辑,访问数据库,进行网络通信,向用户显示信息等。
而其中处理业务逻辑,是我们最需要关注的事情。
由于我们是领域驱动设计,而不是数据表驱动设计,所以我们优先考虑某场景下实体可能有哪些业务行为,而不是实体有哪些属性,也即根据用例流来设计软件。

一、分包示例

如何分包以及组织工程结构?

在DDD的战略设计中,我们关注于从一个宏观的视角俯视整个软件系统,然后通过一定的原则对系统进行子域和限界上下文的划分,即通过软件所实现的业务功能进行模块化划分。

在DDD中,聚合根是主要业务逻辑的承载体,也是“内聚性”原则的典型代表,因此通常的做法便是基于聚合根进行顶层包的划分。

以常见的用户登录网站为例,涉及到的聚合根显然是用户,可分为User包。

DDD将系统分为四种不同的层:

  • 用户界面层(UI) 负责向用户显示信息和解释用户指令,用户不仅仅代表使用界面的人
  • 应用服务层(application) 定义软件要完成的任务,协调下一层领域对象的工作
    • 应用服务实现类(serviceImpl)
    • 事件处理器(handler)
  • 领域层(domain) 负责表达业务概念、业务状态信息以及业务规则
    • 模型(model)
    • 资源库(repository)
  • 基础设施层(infrastructure)为上面各层提供通用的技术能力,包括持久化,传递消息,绘制界面等
    • 领域服务 (service)
    • 仓储实现 (persistence)
    • 防腐层实现 (adapter)

上层可以调用或依赖下层,下层不能依赖上层。

1.简单分包

通常情况,目录组织如下:

├── order
    ├── OrderApplicationService.java
    ├── OrderController.java
    ├── OrderPaymentProxy.java
    ├── OrderPaymentService.java
    ├── OrderRepository.java
    ├── command  //命令对象
        ├── ChangeAddressDetailCommand.java
        ├── CreateOrderCommand.java
        ├── OrderItemCommand.java
        ├── PayOrderCommand.java
        └── UpdateProductCountCommand.java
    ├── exception
        ├── OrderCannotBeModifiedException.java
        ├── OrderNotFoundException.java
        ├── PaidPriceNotSameWithOrderPriceException.java
        └── ProductNotInOrderException.java
    ├── model  //领域模型
        ├── Order.java
        ├── OrderFactory.java
        ├── OrderId.java
        ├── OrderIdGenerator.java
        ├── OrderItem.java
        └── OrderStatus.java
    └── representation  //展现层
        ├── OrderItemRepresentation.java
        ├── OrderRepresentation.java
        └── OrderRepresentationService.java

2.复杂分包

若聚合根复杂程度更高,比如聚合根内聚其他聚合根,则以如下方式组织目录:

├── com.example.user  //顶级模块名+限界上下文
    ├── resource  //用户界面层
    	├── RoleController.java
        └── UserController.java    
    ├── application  //应用层
    	├── command  //命令对象
    		└── CreateUserCommand.java
        ├── representation  //展现层
	        ├── UserRepresentationService.java
	        └── UserSummaryRepresentation.java
	    └── UserApplicationService.java  //应用服务
    ├── domain  //领域层
    	└── model  //领域模型
    		├── user  //分包
    			├── UserId.java  //值对象
    			└── User.java  //聚合根
    		└── role  //分包
    			├── RoleId.java  //值对象
    			└── Role.java  //聚合根
    └── infrastructure  //基础设施层
    	├── services  //领域服务
    		└── MD5EncryptionService.java
    	└── persistence  //持久化
    		└── UserRepository.java  //资源库

注意,上面是复杂情况下的目录组建方式,是分清子领域后的再分层,包内各概念是紧密联系高内聚的。

3.错误分包

不要直接根据分层组织来组建目录,比如这样:

├── com.example.project 
    ├── controller  //用户界面层    	
    	├── OrderController.java
    	├── ArticleController.java
        └── UserController.java    
    ├── application  //应用层
        ├── OrderApplicationService.java  
        ├── ArticleApplicationService.java 
	    └── UserApplicationService.java  //应用服务
    ├── domain  //领域层
    	└── model  //领域模型
    		├── user  //分包
    			├── UserId.java  //值对象
    			└── User.java  //聚合根
    		└── Order  //分包
    			├── OrderId.java  //值对象
    			└── Order.java  //聚合根
    └── infrastructure  //基础设施层
    	├── UserRepository.java  
    	└── OrderRepository.java  

直接把同一领域的概念分散到四个层次中,每个模块都要乘以4,这样关注同一层次的每个类有哪些方法的情况很少出现,这种分包的方式减弱了领域模型的联系,很难跟踪同一概念。

二、分包注意事项

1.组合多个限界上下文的情况

当用户界面需要组合多个限界上下文时,甚至不属于自己领域的外来来源的领域对象时,分包就遇到了困难。

├── com.example.project 
   ├── User  //用户上下文	
   ├── Order //订单上下文
   ├── Article //图文上下文
   ├── Product //产品上下文
   └── ProductArticle  //产品图文上下文
   	├── controller  //控制器
   	└── application   //应用层

这时,可以思考这种情况是否产生了新的领域上下文?
可以另外新建组合包,同时只包含应用层,不包含领域层。
若认为产生了新的领域,则添加领域层。

2.根据聚合进行分包

聚合是边界,聚合根则是对外交互的唯一通道,理应承担整个聚合的实例化工作。若要严格控制聚合的生命周期,可以禁止任何外部对象绕开聚合根直接创建其内部的对象。

可以为每个聚合建立一个包(package),除聚合根之外,聚合内的其他实体和值对象的构造函数皆定义为默认访问修饰符。一个聚合一个包,位于包外的其他类就无法访问这些对象的构造函数。

//在问题的上下文中
package com.example.project.questionContext.domain.model;
//公共访问修饰符,对外交互的唯一通道
public class Question {
	public Question(String title,String description){...}
}

//与问题的上下文在同一个包中
package com.example.project.questionContext.domain.model;
//默认访问修饰符,只允许同一个包中的类访问
public class Answer{
	Answer(String result){...}
}

目录:
领域驱动设计实现疑难解答(二):如何建模
领域驱动设计实现疑难解答(三):如何处理关联
领域驱动设计实现疑难解答(四):如何渲染数据
领域驱动设计实现疑难解答(五):如何发布领域事件

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值