目录
1. 前言
首先,非常感谢各位读者能跟我一起来学习DDD领域驱动设计,以下是我对DDD的一些理解,如果有什么问题,欢迎各位读者留言提出问题。
在学习DDD之前,像大家提出以下几个问题,我们带着问题往下看:
1. 我们学习DDD的目的是什么?只是为了了解这个思想还是想能具体落地实现?
2. 什么是DDD?
3. 我们为什么要使用DDD?
4. DDD的分层结构与MVC的分层结构区别是什么?
5. 什么情况下更适合使用DDD?
6. DDD分层结构中,代码具体是如何存放的?
2. MVC模式与DDD模式
我们先不去了解DDD的概念,首先通过两张图来说明MVC和DDD的分层架构模式
MVC三层开发模式大家应该都非常熟悉,现在公司开发基本都是这种模式。
MVC开发流程:
- 用户需求转化为产品需求
- 需求评审会pm讲解需求转化为研发需求
- 研发人员根据需求进行设计库表结构
- 编写dao层代码
- 编写service代码
- 编写controller代码
从上面的开发流程来看,是不是觉得很容易理解,我们在MVC开发模式下,是不是首先想到的就是数据库怎么设计,数据怎么来,然后dao层编写sql语句接口,数据有了之后,service编写具体业务代码,然后前端需要什么数据格式,controller进行返回,这样 开发起来非常快,但是仔细思考一个问题,如果开发的需求经过不断的迭代之后,代码会怎么样,是不是会变得非常的臃肿,不好维护,甚至不好写单元测试,在测试的时候需要有很多准备工作。
举个例子:
现如今有一个用户注册功能的功能,第一版的情况下什么都不做,只是将数据写入数据看即可,此时service的代码很少,维护起来也方便,但是经过多次迭代之后,可能需要添加注册用户时需要对传过来的数据进行正确性校验,或者验证码的校验等等一系列注册所需要的功能,
此时service的代码会越来越多,一不注意可能改一处动全身,甚至在添加一个功能之后单元测试需要全部一起做,从长远来看随着时间的增长,系统堆了杂七杂八以后,MVC的短板就会日益明显。
很多人都会有一个认为的点,就是认为如果服务很复杂,可以进行服务拆分变成微服务嘛!一样也能很好的维护,开始我也是这么认为的,
单体架构局部业务膨胀可以拆分成微服务,微服务架构局部业务膨胀,又拆成什么呢?比如一个商城系统,一开始就可以很好的设计,用户服务,订单服务,库存服务等等,但是这只是自己理想的拆分方式,当系统越来越大,功能越来越强大的时候,用户服务也会有更多的功能,此时又怎么拆分呢?
DDD就是为了解决这些问题的存在,从一个软件系统的长期价值来看,就需要用DDD,虽然一开始从设计到开发需要成本,但是随着时间的增长,N年以后代码依然很整洁,利于扩展和维护,高度自治,高度内聚,边界领域划分的很清楚。当然了,针对于简单的系统用DDD反而用复杂了,杀鸡焉用宰牛刀!
MVC的开发模式:是数据驱动,自低向上的思想,关注数据。
DDD的开发模式:是领域驱动,自顶向下,关注业务活动。
2.1 总结
如果是一个简单的单体系统,用DDD就把简单问题复杂化了,没必要,但是如果是一个复杂的系统或者微服务而言,从长远角度来看,DDD就是首选了,如果还用MVC模式就会存在以下问题:
- 新需求的开发会越来越难。
- 代码维护越来越难,一个类代码太多,这怎么看对吧,就是一堆屎山。
- 技术创新越来越难,代码没时间重构,越拖越烂。
- 测试越来越难,没办法单元测试,一个小需求又要回归测试,太累。
学习了这些之后再来学习DDD领域驱动将会事半功倍,接下来就正式进入DDD领域驱动设计的学习中
3. DDD领域驱动设计
3.1 简介
DDD是领域驱动设计(Domain-Driven Design)的缩写,这是一种主要软件开发方法,由Eric Evans在它的书《领域驱动设计:软件核心负责性应对之道》中首次提出。DDD主要关注于创建与业务领域紧密相关的软件模型,以确保能够准确地解决实际问题。
3.2 DDD的核心概念
DDD 的核心知识体系,具体包括:领域、子域、核心域、通用域、支撑域、限界上下文、实体、值对象、聚合和聚合根等概念。
DDD即领域驱动设计,是一种软件开发方法乱,旨在通过将复杂设计聚焦于领域模型上,来简化软件项目的开发和管理,特别是对于复杂业务环境的系统开发。
一句话描述:通过边界划分将复杂业务领域简单化,建立高扩展、高内聚、低耦合的系统架构。
一般做DDD设计可以从战略设计和战术设计两方面入手,在了解这两种设计方式之前,我们先了解下几个概念:
领域
百度百科对领域的解释:“领域具体指一种特定的范围或区域。”
其实领域就是确定范围边界,可大可小,比如商城是一个较大领域,业务范围较广。
子域
子域和领域其实本质上是一个概念,只是大小的区分,比如在商城领域下可以细分成多个子域,比如:订货域
子域还可以细分到不同的问题域、更细的业务范围。
核心域
完成业务活动必须的子域,比如在商城中的采购、仓储、订单等子域都是核心域
通用域
被多个核心域使用的业务子域,如用户就属于通用域。
支撑域
为业务子域提供基础服务的,如日志、数据存储等。
限界上下文
用来封装通用语言和领域对象,提供上下文环境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。这个边界定义了模型的适用范围,使团队所有成员能够明确地知道什么应该在模型中实现,什么不应该在模型中实现。
实体
实际上就是在开发过程中的实体类,例如User、People等
官方解释,实体和值对象会形成聚合,每个聚合一般是在一个事务中操作,一般都有持久性操作。聚合中,跟实体的生命周期决定了聚合整体的生命周期。说白了,就是对象之间的关联,只是规定了关联对象规则(必须是由实体和值对象组成的),操作聚合时类似hibernate的One-Many对象的操作,一起操作,不能单独操作。
值对象
官方解释,描述了领域中的一件东西,将不同的相关属性组合成了一个概念整体,当度量和描述改变时,可以用另外一个值对象予以替换,属性判等,固定不变。
个人理解就是实体中的属性字段,例如User中的id、name等
聚合
在DDD中,实体和值对象是很基础的领域对象。实体一般对应业务对象,它具有业务属性和业务行为;而值对象主要是属性集合,对实体的状态和特征进行描述。但实体和值对象都只是个体化的对象,它们的行为表现出来的是个体的能力。
那么聚合就是将各个实体和值对象组合起来,比如企业单位,每个人都是一个实体,而整个单位则是一个聚合。
在标准的DDD设计中,聚合是修改和持久化数据的基本单元,聚合内部是高内聚的,而聚合之间是松耦合的,我们平常最多使用的应该就是一个实体一个聚合。
聚合根
聚合根,很明显就是聚合的唯一标识符,是整个聚合顶部的组织实体,即管理实体,实际使用上操作聚合都是通过聚合根实现。如果把聚合比作组织,那聚合根就是这个组织的负责人。
3.3 领域模型
在领域模型中分为四大类:失血模型、贫模型、充血模型和胀血模型,想要理解这几个分类,先要知道“血”指的是Domain Object的Domain层内容。
3.3.1 失血模型
Domain Object(领域对象)模型仅仅包含对象属性的定义和操作对象属性的getter/setter方法,所有的业务逻辑完全由Business Logic层(业务逻辑层)中的服务类来完成。这种类在java中叫POJO,在.NET中叫POCO。
优点
- 领域对象结构简单
缺点
- 肿胀的业务服务代码逻辑,难于理解和维护
- 无法良好的应对复杂业务逻辑和场景
代码示例
// User.java (失血模型)
public class User {
private Long id;
private String name;
private String email;
// Getters and Setters
}
3.3.2 贫血模型
贫血模型(Anemic Domain Model)是一种领域模型,其中领域对象(Model)中仅包含数据(状态),而不包含业务逻辑(行为)。这种模型设计方式将数据和操作分离,通常将数据访问逻辑放在数据访问层(DAO),业务逻辑放在服务层(Service)。贫血模型的领域对象通常只有基本的属性和对应的getter/setter方法,而不包含任何业务逻辑上的方法。这种设计模式有时也被称为事务脚本模式(Transaction Script),它与面向对象设计的基本思想相悖。与失血模型相比,贫血模型至少提供了数据访问的方法,但仍然缺乏领域特定的行为。这种模型广泛应用于MVC模式中
User
// User.java (贫血模型)
public class User {
private Long id;
private String name;
private String email;
// Getters and Setters
}
Service
// UserService.java
public class UserService {
public void registerUser(User user) {
// 业务逻辑:注册用户
// 这里可能会涉及到验证、密码加密、保存到数据库等操作
}
public void updateUserEmail(User user, String newEmail) {
// 业务逻辑:更新用户邮箱
// 这里可能会涉及到验证用户身份、更新数据库等操作
}
}
3.3.3充血模型
定义及特点
充血模型(Rich Domain Model),也称为领域模型(Domain Model),是领域驱动设计(Domain-Driven Design, DDD)中的一个核心概念。在充血模型中,领域对象不仅包含数据(属性),还包含行为(方法),这些行为定义了对象如何操作和维护其数据,以及如何与其他对象交互。
充血模型的特点:
- 封装性:充血模型将数据和行为封装在同一个对象中,遵循面向对象设计的封装原则。
- 业务逻辑的集中:对象的行为(业务逻辑)直接定义在对象内部,而不是分散在外部的服务层或工具类中。
- 实体和值对象:充血模型中通常包含实体(Entity)和值对象(Value Object)。实体具有唯一标识,而值对象则描述了对象的属性或状态,没有唯一标识。
- 行为丰富:充血模型的对象通常包含丰富的行为,这些行为定义了对象如何响应业务操作。
- 领域逻辑的直接表达:充血模型使得领域逻辑可以直接在领域对象中表达,便于理解和维护。
充血模型的优点:
- 提高代码的可读性和可维护性:因为业务逻辑封装在领域对象中,所以代码更加直观,易于理解和维护。
- 促进领域逻辑的复用:领域对象可以被多个客户端共享和重用,减少了代码的重复。
- 增强领域模型的测试性:充血模型的对象通常更容易进行单元测试,因为它们包含了完整的业务逻辑。
- 支持领域驱动设计:充血模型是DDD实践的基础,有助于团队更好地理解和建模业务领域。
充血模型的缺点:
- 设计复杂性:相比于简单的数据传输对象,充血模型需要更多的设计工作,以确保对象的行为正确且符合业务需求。
- 可能引入过多的复杂性:在一些简单的应用中,充血模型可能会引入不必要的复杂性。
User
// User.java (充血模型)
public class User {
private Long id;
private String name;
private String email;
// Getters and Setters
// 业务逻辑方法
public void register() {
// 用户注册逻辑
}
public void updateEmail(String newEmail) {
// 更新邮箱逻辑
// 这里可能会涉及到验证新邮箱格式、更新数据库等操作
this.email = newEmail;
}
}
Service
public class UserService {
public void registerUser(User user) {
user.register();
}
public void updateUserEmail(User user, String newEmail) {
user.updateEmail(newEmail);
}
}
3.3.4 胀血模型
定义
胀血模型是充血模型的一种极端形式,其中领域对象包含过多的业务逻辑,导致对象过于庞大和复杂。这种模型可能会导致单个对象承担过多的职责,从而违反了单一职责原则。
特点:
- 领域对象包含过多的业务逻辑,可能涉及多个不同的业务领域。
- 可能导致对象难以理解和维护。
- 可能需要重构以简化对象和它们的职责
public class User {
private String username;
private String password;
private String email;
private String address;
private int age;
// 用户的基本信息
public User(String username, String password, String email, String address, int age) {
this.username = username;
this.password = password;
this.email = email;
this.address = address;
this.age = age;
}
// 标准的getter和setter方法
...
// 用户行为
public void login() {
// 登录逻辑
}
public void logout() {
// 注销逻辑
}
// 业务逻辑
public void placeOrder(Product product) {
// 下单逻辑
}
// ...可能还有更多方法
}
// 其他相关类
class Product {
// 商品信息
}
class Coupon {
// 优惠券信息
}
class Order {
// 订单信息
}
3.4 防腐层
当某个功能模块需要依赖第三方系统提供的数据或者功能时,我们常用的策略就是直接使用外部系统的API、数据结构。这样存在的问题就是,因使用外部系统,而被外部系统的质量问题影响,从而“腐化”本身设计的问题。
因此我们的解决方案就是在两个系统之间加入一个中间层,隔离第三方系统的依赖,对第三方系统进行通讯转换和语义隔离,这个中间层,我们叫它防腐层。
从图中可以看出,就是在两个系统之间加了一层,两个子系统语义解耦
防腐层作用:
- 使两方的系统解耦,隔离双方变更的影响,允许双方独立演进;
- 防腐层允许其它的外部系统能够在不改变现有系统的领域层的前提下,与该系统实现无缝集成,从而降低系统集成的开发工作量。
3.5 落地实现
3.5.1 战略设计
一句话:我们要做什么
个人理解是从整体宏观界面进行设计,划分限界上下文并建立领域模型的过程,比如在商城中可以从整体上划分采购、仓管、订单等业务领域
举个例子:实现祖国个全面统一,收复台湾,就是一个战略
3.5.2 战术设计
战略设计是从宏观层面进行设计,而战术设计则更加贴近现实,主要是如何去落地。
从技术视角出发,侧重于领域模型的技术实现,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计和实现。
大白话就是如何落地战略设计中已经划分好的领域,这个比较贴近一线开发人员
4. DDD架构
分层架构的一个重要原则是每层只能与位于其下方的层发生耦合,较低层绝不能直接访问较高层。分层架构可以简单分为两种:
严格分层架构:
某层只能与位于其直接下方的层发生耦合
松散分层架构:
则允许某层与它的任意下方层发生耦合
我们在实际运用过程中多使用的是松散分层架构。
4.1 实践
看到这里,想必读者已经对DDD的概念有所了解,但是不知道代码究竟是如何存放的,四层架构需要有些什么包,类,我知道你很急,但是你先别急,接下来就带大家进行从MVC架构模式-DDD架构模式的演练
说明:以下层级划分只是我对DDD的理解进行的,并不一定必须按照这个要求来
4.1.1 MVC架构
User
@Data
public class User {
private Long id;
private String name;
private Integer age;
private String phone;
private String address;
private String email;
private String password;
private Integer status;
}
UserController
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
@PostMapping("/register")
public String register(@RequestBody User user) {
// 调用service层方法进行注册
userService.register(user);
return "注册成功";
}
}
UserService
public interface UserService {
void register(User user);
}
UserServiceImpl
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserDao userDao;
@Override
public void register(User user) {
//其余业务逻辑
......
// 插入用户信息到数据库
userDao.insert(user);
}
}
UserDao
@Mapper
public interface UserDao {
// 插入用户信息到数据库
void insert(User user);
}
从上面的代码中可以看出,所有的业务逻辑全部写在service中,此时,注册用户不单单只是将信息保存,在保存之前还需要有更多的功能,比如校验,添加验证码等等,如果代码迭代次数少还好,service代码不是很多,但是经过多次迭代之后,代码会很臃肿,所有代码都在service中,改一个地方需要全方面的进行测试
4.1.2 DDD架构
主要分为四层:
interface:接口层(包含req:前端传的参数实体类,vo:返回前端的实体类 controller:与前端交互的接口)
application:应用层(包含dto:调用domain层传的参数实体,service:没有具体核心业务逻辑,只是业务的一个编排等代码)
domain:领域层:(包含entity:与数据库一一对应的实体,贫血模型,service:核心业务逻辑,相当于mvc的service,Repository:是一个接口,与数据库交互的中间层,个人理解是防腐层)
infrastructure:基础设施层(包含domain中Repository的实现类,mapper:相当于mvc的dao,以及存放其余的一些第三方工具,缓存等)
接口层
UserController
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserApplicationService userDomainService;
/**
* 测试ddd代码分层结构
*/
@GetMapping("/register")
public ResultVo register(@RequestBody UserRequest req) {
userDomainService.register(req);
return new ResultVo();
}
}
UserRequest
@Data
public class UserRequest {
private String name;
private Integer age;
private String phone;
private String address;
private String email;
//充血模型 validate演示改实体类的行为
public void validate() {
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("用户名不能为空");
}
if (age == null || age < 0) {
throw new IllegalArgumentException("年龄不能为空");
}
if (StringUtils.isBlank(phone)) {
throw new IllegalArgumentException("手机号不能为空");
}
if (StringUtils.isBlank(address)) {
throw new IllegalArgumentException("地址不能为空");
}
if (StringUtils.isBlank(email)) {
throw new IllegalArgumentException("邮箱不能为空");
}
if (!email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")) {
throw new IllegalArgumentException("邮箱格式不正确");
}
if (!phone.matches("^1[3-9]\\d{9}$")) {
throw new IllegalArgumentException("手机号格式不正确");
}
if (name.length() > 20) {
throw new IllegalArgumentException("用户名不能超过20个字符");
}
if (address.length() > 50) {
throw new IllegalArgumentException("地址不能超过50个字符");
}
if (email.length() > 50) {
throw new IllegalArgumentException("邮箱不能超过50个字符");
}
if (phone.length() > 11) {
throw new IllegalArgumentException("手机号不能超过11个字符");
}
if (age > 150) {
throw new IllegalArgumentException("年龄不能超过150岁");
}
if (age < 0) {
throw new IllegalArgumentException("年龄不能小于0");
}
}
}
充血模型 validate演示该实体类的行为方法
ResultVo
只是一个返回前端的vo实体,自定义即可
应用层
UserApplicationService
@Service
public class UserApplicationService {
@Resource
private UserDomainService userDomainService;
/**
* 应用层不做核心具体的业务逻辑,只做业务逻辑编排
* @param req
*/
public void register(UserRequest req) {
// 1. 数据校验
req.validate();
// 2. 创建用户注册实体
UserDto dto = new UserDto();
BeanUtils.copyProperties(req, dto);
//其余业务逻辑,也是通过方法调用进行,具体业务逻辑在对应类中
......
// 3. 调用领域服务方法
userDomainService.registerUser(dto);
}
}
从上面代码可以看出,该类与mvc的service不同,mvc的service核心逻辑都在里面,但是该类没有具体业务,只是做了编排,123步都是调用其余类的方法,这样如果做改动只需要去对应的类进行就可以,并且进行单元测试也只需要调用对应类的方法即可
UserDto
@Data
public class UserDto {
private String name;
private Integer age;
private String phone;
private String address;
private String email;
}
该类是与domain层传递参数的实体
领域层
UserDomainService
public interface UserDomainService {
void registerUser(UserDto dto);
}
UserDomainServiceImpl
@Service
public class UserDomainServiceImpl implements UserDomainService {
@Resource
private UserRepository userRepository;
@Override
public void registerUser(UserDto dto) {
//具体注册的业务逻辑
User user = new User();
BeanUtils.copyProperties(dto, user);
//不直接调用基础设施层的mapper接口插入数据,而是间接调用Repository,可以在Repository实现类中写入之前做一些处理
userRepository.save(user);
}
}
User
@Data
public class User {
private Long id;
private String name;
private Integer age;
private String phone;
private String address;
private String email;
private String password;
private Integer status;
}
与数据库表字段一一对应的实体类
UserRepository
public interface UserRepository {
void save(User user);
}
基础设施层
UserRepositoryImpl
@Repository
public class UserRepositoryImpl implements UserRepository {
@Resource
private UserMapper userMapper;
@Override
public void save(User user) {
//基础设施层操作数据库
userMapper.save(user);
}
}
实现领域层的UserRepository接口
UserMapper
@Mapper
public interface UserMapper {
void save(User user);
}
xml文件可以存放在基础设施层也可以放在resources目录下
进行与数据库交互的类
说明:
- 基础设施层还能存放其余类,比如工具包,缓存,以及其它的基础的文件,第三方工具,网关等等
- 以上只是我对DDD架构的简单划分,实际开发中,还有不同的划分,具体根据业务场景、限界上下文来
后话
以上是自己对DDD的理解,如果不同的观点,或有什么错误之处,欢迎留言指出,一起学习!!!