DDD(1)-领域驱动设计整体理解

前言

如何理解领域驱动设计?抛开各种概念、工具、方法,单纯从“领域驱动设计”这个标题来理解,这是第一个主谓宾结构,意思是用领域来驱动软件设计,是一种软件设计思想。如何理解领域、驱动呢?为什么要使用领域驱动设计呢?如何实现领域驱动设计呢?

驱动

首先探讨下驱动的内涵。个人理解,驱动指明了两个事物间的传递关系。我们说A驱动了B,这表达了两个含义:1、B依赖于A;2、A的变动会导致B的变动。因此“领域驱动设计”表明了领域是核心,先有领域,后才有软件设计。只允许领域的变动引起软件的变动,而不能让软件的变动引起领域变动。

领域即业务

软件就是系统,是程序和数据,那什么是领域呢?首先我们明白“领域驱动设计”也是一种软件设计方法,而软件设计就是产出一个系统来解决业务问题,核心概念就是:业务、软件。那么领域就只能对应到业务问题 这个概念上了。因此“领域驱动设计”可以理解成“业务驱动设计”,这同样包含两个约束:1、软件依赖业务存在;2、业务的变动引起软件的变动,软件的变动不能引起业务的变动。
业务是现实世界中的经营流程,需要靠开发人员映射成软件系统中的代码,即系统中的部分代码=业务,这里之所以说是部分代码,是因为一个完整的软件系统除了描述业务,还有相当部分的软件间通信、数据存储、安全、性能等方面的支撑代码。现有的软件开发方式天然满足约束1,但由于支撑代码的存在且和业务代码紧密耦合,导致并不满足约束2中的“软件的变动不能引起业务(业务代码)的变动”。

不使用业务驱动设计导致的问题

现在的大多数企业级系统是分层架构,三层或四层。上层依赖下层,最底层是数据层,又由于Java的静态类型语言,导致底层的依赖会逐层传递到上层,一个经典的例子是数据表的映射对象DO会一直传递到接口层。业务代码和支撑代码越来越紧密地耦合在一起,一段代码即描述业务流程,又包含各种技术实现,造成的后果就是随着需求的变动,系统复杂性越来越高。
究其原因有两点:1、分层结构无法阻断依赖向上传递2、业务的复杂度和技术复杂度通常变动速率不一致
最底层的数据访问层通常建立在关系型数据之上,而且数据库一旦选型几乎就不再变动,这就导致与之对应的DAO层也接近于固化,变成“固件”,不易改变。而软件最重要的特性就是可以方便的改变,业务变动引起的上层原件变动和底层固件的“不可变性”形成矛盾,这种矛盾最终依靠开发人员编写大量适配的胶水带来来缝合,导致业务代码和技术实现代码越来越耦合。而且随着人员的变动,新接手的程序员为了减少引入故障,通常倾向于新写一套业务+技术实现的代码来实现需求,而不是在原来的基础上改造,这进一步加剧了代码量、耦合程度。
业务含义通常需要更大比例的技术代码来实现支撑。例如某系统初期只有读+写两个接口,使用Mysql实现数据持久化,当接口数膨胀到十几个,各种写入、查询,使得业务代码、技术代码数量快速增长。后期为了提高性能,引入Redis作为缓存组件,则需要在膨胀后的业务代码中编入缓存的读写逻辑,业务代码被拆解散落在各种片段中,导致系统理解困难、维护困难,最终导致进入不可控地步。

领域驱动设计如何解决

领域驱动设计为解决这个问题提出了一种思考方向,那就是抽离出业务代码,形成单独的领域层。领域层作为软件系统的核心,所有其他软件部分依赖领域层,只有领域层的变动才能引起其他部分的变动,而将其他部分的变动隔离在领域层之外。典型如六边形架构:
在这里插入图片描述
图中领域模型部分就是业务代码,其只负责描述业务,而相关支撑性代码都在外围,并通过依赖倒置的方式实现隔离。如果我们只关注领域模型部分,其中的代码就是对业务的翻译。
领域驱动设计方法并没有降低复杂度,业务的复杂度是客观存在的,通过领域驱动设计的方法只是实现了复杂度的可控,这种可控是通过分离业务和技术实现完成,并且还通过领域建模等手段,将业务也进行了拆分,也就是常说的子域、界限上下问题、聚合等概念,在后续的文章会介绍,现在我们只需要知道,领域驱动设计的核心手段就是分离,横向上把业务和技术实现拆分开来,垂直向上把业务实现拆分。最终的结果就是把一个复杂系统拆成了多个子模块,达到人脑能一次读取、理解、改造的规模,进而提高的系统的可维护性。

代码示例

举个用户登录例子,需求描述为:
1、用户输入用户名、密码进行登录匹配数据库密码记录登录失败提示“用户名或密码”
2、登录成功后返回用户昵称、头像调用浏览记录服务的Rpc接口获取最近浏览记录
3、记录本次浏览记录到Mysql数据库,并在Redis中更新浏览次数缓存
描述中的黄色文字是原始业务诉求,斜体部分则是技术实现方案。非DDD架构的代码描述可能为:

//查询数据库比对密码
Long userId = userDao.getUserId(userName,PasswordUtil.signature(password));
if(userId==null){
	throw new BizException("用户名或密码错误");
}
UserVO userVO= new UserVO();
//查询user表,获取个人信息
User user = userProfileDao.getUser(userId);
userVO.setNickName(user.getNicName());
userVO.setImg(user.getImg());
//调用远程服务获取历史浏览记录
Result<List<HistoryDTO>> result = historyService.historyService.getUserHistory(userId)
if(result!=null && result.isSuccess()){
	List<History> list = result.getData();
	if(list!=null){
		userVO.setHistory(list.stream().map(item->{
			HistoryVO vo = new HistoryVO();
			vo.setUserId(item.getUserId);
			vo.setVideoName(item.getVideoName);
			//...其他字段
			vo.setTime(DateUtil.toStr(item.getTime));
			return vo;
		}).collect(Collectors.toList()));
	}
}
//新增浏览记录到数据库
History history = new History();
history.setUserId(userId);
history.setChannel(getCurrentChannel());
historyDao.insert(history);
//redis中浏览次数+1
redisClient.incrt(userId,1);
return userVO;

相信大家对这种代码是非常熟悉的,这里不考虑代码质量优劣,这段代码最大的问题是直接依赖了第三方服务History的接口、Mysql数据库、Redis中间件,并且把具体的技术实现和业务代码融合在一起。后续再增加新的需求,会带来更多业务代码、技术代码,上千行的类、方法就非常容易出现。任何一个新接手的人都需要把整体代码全部读入脑中,从中提取、分离出哪些是业务逻辑,哪些是技术实现,才能有可能继续添砖加瓦,而且任何的外部变动都会引起代码的改变。

领域层示例

如果使用领域驱动设计,代码表述可能是这样的:

//登录失败会内部抛出异常,成功则返回用户信息
User user = userService.login(userName,password);
//获取用户历史记录
List<History> history = history.getHistory(user);
//记录本次登录
history.hasLogin(user);
//返回
return new UserLogin(user,history);

代码中每一行都对应一条业务描述,先关的方法会定义成接口,由外部模块依赖领域层提供技术实现,以此实现业务和技术实现的隔离。这样的好处是,业务代码可以紧密跟随业务变动,而在实现类中完成适配,把原来一团耦合在一起的代码分割到小的范围分而治之,范围越小,越能容易被人脑理解,也就越容易迭代。

代码层次结构

那么如何实现领域驱动设计所提出的分离思想呢?毕竟如果只靠DDD的知识传递,希望大家都遵守面向接口编程的设计规范,在实践中是一个巨大的难题,我们需要一些强有力的约束保障。在Java工程中控制层级之间的依赖是一个可行办法。
根据目前大厂和相关软件设计专家的一些实践经验,基于Maven的module控制层级之间的依赖关系,由此形成的四层架构是一个比较好的落地
在这里插入图片描述
其中的Domain就是领域层,其是一个非常纯净的Maven module,不依赖任何第三方jar包(工具类除外)。

总结

上面探讨了些我个人理解的领域驱动设计的内涵,并没有涉及各种概念、实践方法,只是从“为什么需要领域驱动设计”出发,分析了当前软件设计遇到的问题以及领域驱动希望如何解决。从理念到落地还有相当长的路要走,也有很多方法、工具可以使用,例如CQRS设计、事件风暴、边界划分等,而这些恰恰是《领域驱动设计》作者并未给出的部分,这也导致DDD提出来这么多年,对它的评价两极分化,不过随着近些年一些软件设计专家的分享和大厂的实践,逐渐形成了一些共识的方法,例如从原版的领域层依赖基础设施层的四层架构演进到如今的六边形架构,后续会继续分享DDD落地过程中的一些有益思考和方法。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值