大话领域驱动设计中的贫血模型和充血模型

2023-10-08 21:59

本文主要是介绍大话领域驱动设计中的贫血模型和充血模型,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、前言

领域驱动设计(DDD)作为一种软件设计思想,在近几年日益复杂的系统架构演变中重新被人拿出来讨论,特别是在当下非常流行的微服务架构中,DDD的价值更加突显出来。大部分人对DDD的认识,都是来自于Eric Evans在2004年出版的《领域驱动设计——软件核心复杂性应对之道》,可以说这本书为DDD在整个业界奠定了基础,十几年后的今天大家依然在这个基础上沿用了很多概念,只是在一些细节上不断进行改进,可见Eric Evans大师的远见。

DDD虽然给人们提供了一种以领域为中心来设计的原则性指导,但具体的分层和实现,会因不同的团队的理解和习惯、先进技术的出现而不断演进。就如最早的分层架构是领域层依赖基础设施层的,但最新流行的分层架构是通过领域层的抽象接口来实现对基础设施层的依赖反转,实现领域层作为最核心最稳定的层次不依赖于其他的层次。

话不多说,先呈上经典的领域分层架构图。

讨论领域分层架构的文章有很多,DDD经典四层架构、改进后的五层架构、六边形架构,我们团队也结合团队自身的情况对领域分层架构展开了多轮讨论,最终一致决定选择在经典四层架构基础上,将领域层对基础设施层依赖反转,作为我们的初版分层架构,以后再按需演进。讨论过程中,针对领域模型应该使用贫血模型还是充血模型的问题,是我们形成两种意见的最大分歧点,这里整理了一下对这两种模式的模型的思考,分享给大家。所以这篇文章只探讨领域模型的模式问题。

二、领域模型的四种模式

在讨论领域模型的模式问题前,我们先简单定义一下失血、贫血、充血、胀血这四种实体模式,因为我曾看到有人把失血模式的模型也称之为贫血,然后尽数贫血模式的各种危害。我们也不是定义标准的人,只是想在本文章讨论范围先有一个统一的认知。

失血模型

失血模型简单来说就是模型中只有属性的setter和getter方法,并且只是简单的赋值或直接返回属性值,是对一个实体类最简单的封装,其他所有的业务行为和数据存储由专门的服务类和DAO来完成。以一个Person类为例,包含姓名、生日、年龄的简单属性,只有判断今天是否生日和过生日(过生日后年龄会+1)两个行为。

public class Person extends Entity {/*** 姓名*/private String name;/*** 生日*/private Date birthday;/*** 年龄*/private Integer age;public String getName() {return name;}public void setName(String name) {this.name = name;}public Date getBirthday() {return birthday;}public void setBirthday(Date birthday) {this.birthday = birthday;}public Integer getAge() {return age;}public void setAge(Integer age) {this.age = age;}
}public interface PersonService {/*** 判断今天是否生日*/boolean isTodayBirthday(Long personId);/*** 生日快乐,长一岁,age+1*/void happyBirthday(Long personId);
}

贫血模型

贫血模型是在失血模型的基础上聚合了对应领域范畴的业务领域行为,不仅仅是简单的setter/getter,但在行为过程中对领域对象的状态发生的变化只停留在内存层面,不关心其数据的持久化,即不依赖Repository/DAO,把数据持久化放在service中按需处理。

public class Person extends Entity {/*** 姓名*/private String name;/*** 生日*/private Date birthday;/*** 年龄*/private Integer age;/*** 判断今天是否生日*/public boolean isTodayBirthday() {SimpleDateFormat sdf = new SimpleDateFormat("MM-dd");return sdf.format(birthday).equals(sdf.format(new Date()));}/*** 生日快乐,长一岁,age+1*/public void happyBirthday() {age++;}public String getName() {return name;}public void setName(String name) {if (StringUtils.isBlank(name)) {// throw a exception}this.name = name;}public Date getBirthday() {return birthday;}public void setBirthday(Date birthday) {if (birthday == null) {// throw a exception}this.birthday = birthday;}public Integer getAge() {return age;}public void setAge(Integer age) {if (age == null || age < 0) {// throw a exception}this.age = age;}}

充血模型

充血模型是在贫血模型的基础上,依赖repository,在业务领域行为执行过程中也负责数据的持久化存储。

public class Person extends Entity {@Autowiredprivate PersonRepository personRepository;/*** 姓名*/private String name;/*** 生日*/private Date birthday;/*** 年龄*/private Integer age;/*** 判断今天是否生日*/public boolean isTodayBirthday() {SimpleDateFormat sdf = new SimpleDateFormat("MM-dd");return sdf.format(birthday).equals(sdf.format(new Date()));}/*** 生日快乐,长一岁,age+1*/public void happyBirthday() {age++;// 数据持久化personRepository.update(this);}public String getName() {return name;}public void setName(String name) {if (StringUtils.isBlank(name)) {// throw a exception}this.name = name;}public Date getBirthday() {return birthday;}public void setBirthday(Date birthday) {if (birthday == null) {// throw a exception}this.birthday = birthday;}public Integer getAge() {return age;}public void setAge(Integer age) {if (age == null || age < 0) {// throw a exception}this.age = age;}}

胀血模型

这种模式下连service都不需要了,所有的业务逻辑、数据存储都放到一个类中。

public class Person extends Entity {@Autowiredprivate PersonMapper personMapper;/*** 姓名*/private String name;/*** 生日*/private Date birthday;/*** 年龄*/private Integer age;/*** 判断今天是否生日*/public boolean isTodayBirthday() {SimpleDateFormat sdf = new SimpleDateFormat("MM-dd");return sdf.format(birthday).equals(sdf.format(new Date()));}/*** 生日快乐,长一岁,age+1*/public void happyBirthday() {age++;modify();}/*** 查询*/public static Person getById(Long id) {PersonDO personDO = personMapper.selectById(id);Person person = PersonConvertor.toPerson(personDO);return person;}/*** 新增*/public static Person create(String name, Date birthday, Integer age) {// 这里省略了事务管理,入参校验等,相当于service层需要做的都在这里处理Person person = new Person();person.setName(name);person.setBirthday(birthday);person.setAge(age);personMapper.insert(person);return person;}/*** 修改*/public void modify() {// 这里省略了事务管理,入参校验等,相当于service层需要做的都在这里处理personMapper.update(this);}public String getName() {return name;}public void setName(String name) {if (StringUtils.isBlank(name)) {// throw a exception}this.name = name;}public Date getBirthday() {return birthday;}public void setBirthday(Date birthday) {if (birthday == null) {// throw a exception}this.birthday = birthday;}public Integer getAge() {return age;}public void setAge(Integer age) {if (age == null || age < 0) {// throw a exception}this.age = age;}}

对于DDD来说,失血模型和胀血模型无疑是不合适的,一个太轻没有聚合领域行为又回到以前的数据驱动,一个太重导致领域模型变得极不稳定,失去了领域模型的意义。而贫血模型和充血模型更加符合面向对象,把领域中核心的业务逻辑聚合在领域模型中高度复用,只是需要合理划分哪些业务逻辑放在模型中,哪些业务逻辑放在业务逻辑层中,这是这两种模型共同存在着的一个大挑战,需要开发团队有统一合理的业务认知并且守住底线,持续严格遵守。这个不在本文的讨论范围就不多说了。

三、贫血 or 充血?

那贫血模型和充血模型之间到底选择哪种模式?我们从以下四个维度来讨论。

缩小依赖范围

对于业务复杂的系统来说,维护一个大而全的领域模型是非常不明智的,从单体架构到微服务架构,都是一个不断缩小依赖范围的过程,这是一个很好的思路。DDD体系就是这种思想的驱动者,甚至细致到每一个微服务的内部的编码层面,倡导明确划分界限上下文,尽量以最小单元各司其职。

我们很容易可以看出,贫血模型和充血模型最显而易见的区别就是数据存储在不在领域模型里面完成,换言之就是领域模型有没有依赖Repository。所以从依赖范围来看,贫血模型的依赖范围是更小的。当然在这里也要为充血模型打抱不平一下,在最早的DDD架构中,Repository的接口是定义在基础设施层,如果领域模型中依赖了Repository,就相当于最核心的领域模型直接依赖了基础设施层,甚至形成双向依赖,这个肯定是不能接受的。但是现在通过依赖反转,Repository的接口定义在领域层,实现在基础设施层再通过依赖注入到领域模型中,所以充血模型即使依赖了Repository也只是依赖了同一层内部的接口,层与层之间不至于会形成双向依赖。但是相比之下,还是贫血模型更加简单,更加能体现领域模型是在最核心最底层的模块。

另外,贫血模型不依赖Repository,对于单元测试更加有优势,连Repository接口都不需要mock了,更加可以脱离框架容器来进行测试,因为剩下的都是最纯粹的可测试的领域逻辑。

关注点分离

关注点分离有时候会和单一职责原则一起拿出来说,关注点分离是对处理软件复杂性的指导原则,而单一职责是细致到指导如何面向对象设计一个类时的原则,他们都有一个共同的思想,就是解耦和增强内聚性,也就是我们经常说的“高内聚,低耦合”。

回到领域模型,领域模型的最主要职责应该是对一个业务领域的逻辑抽象,它应该是可以独立不依赖任何技术框架而存在的。贫血模型和充血模型的区别是数据的持久化存储,如果是最纯粹的业务逻辑为什么要关心数据的存储呢?假设系统的运行内存是无限大的话,是不是不持久化存储也可以照常运行呢?可能有人会说这是在脱离实际说话,不过我是想说我们只是在用一个极端条件来探讨一个理论概念,从而明确理论指导的正确性。我们实际开发中当然需要考虑数据的持久化存储,但我们是否可以把数据存储从领域模型中分离出来呢?我认为是可以的。

我们先明确一点,与领域模型相关的数据操作应该是与其领域对象的状态相关并只与这一个对象相关(如修改这个对象本身的信息/状态/包含的关系等),充血模型依赖的Repository做的也应该是这里业务相关的存储。而与这单个对象状态不相关的数据操作,如查询、跨多个模型的数据操作、对同一个模型的一批对象的数据操作,是不应该放在充血模型依赖的Repository里面的。这么看来,如果使用充血模型,我们对数据的操作是既要落在领域模型里面,也要落在模型之外的各种服务方法里,而且这些地方都要关注事务管理的问题,这就变得关注点比较杂多了。这样是不是使用贫血模型,只对数据操作停留在内存状态层面,输出有状态的对象,由专门处理事务的服务方法去进行数据存储好一些呢?

可复用性

DDD在统一领域语言、明确划分界限上下文、领域之间尽量避免存在重复的业务逻辑这些方面就充分体现了可复用性这一点。

在可复用的角度来看贫血模型和充血模型,更纯粹的贫血模型无疑是可复用性更高的,如果我想复用领域模型中的某个修改状态的方法,只是想获取变更后的状态,并不想将它持久化,那么对于充血模型的这个方法就不能复用了。

一般来说,职责过多、关注点过多的方法,都不利于提高可复用性,这点与上面讨论的关注点分离也是能相呼应的。

向稳定的方向依赖        

无论是哪个版本的DDD分层架构图,领域层都是最核心最底层的,而领域模型则是核心中的核心,因为领域模型应该是整个系统中最稳定的一块,这也是它的价值所在。所以我们才会在一开始花大量的时间对领域建模,就是希望设计出稳定健壮的、可以应对后面各种业务变化的领域模型。试问如果一个软件系统,最核心的业务也扛不住变化的话,这个系统还有什么意义?

既然领域模型是我们系统的最核心模块,那么所有的依赖方向都应该是指向它的,否则无法保证它的稳定性。而充血模型依赖了Repository接口,还是会有破坏稳定性的风险。有人会提出,只是依赖了同一层中的接口,又不是实现,应该还好吧。即使是接口,这个调用过程的入参出参也无形让他们之间建立了各种联系,而Repository接口的入参出参是与数据存储结构密切相关的,而我理解领域模型可以不一定与数据存储结构紧密相关,最好是可以脱离数据存储结构,因为在系统演进过程中,数据存储结构有一些小调整是常有的事情。

到这里,我们最终在贫血模型和充血模型之间的选择已经很明显了,但是这是不是最好的选择呢?不一定,有很多结论还是要通过大量的实践、架构的演进、技术的改进中,一一去论证。但是,我们必须找到当下认为的最优解去实施落地,开始旅程,否则都是无尽的讨论。

 

这篇关于大话领域驱动设计中的贫血模型和充血模型的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/168264

相关文章

Python基于火山引擎豆包大模型搭建QQ机器人详细教程(2024年最新)

《Python基于火山引擎豆包大模型搭建QQ机器人详细教程(2024年最新)》:本文主要介绍Python基于火山引擎豆包大模型搭建QQ机器人详细的相关资料,包括开通模型、配置APIKEY鉴权和SD... 目录豆包大模型概述开通模型付费安装 SDK 环境配置 API KEY 鉴权Ark 模型接口Prompt

大模型研发全揭秘:客服工单数据标注的完整攻略

在人工智能(AI)领域,数据标注是模型训练过程中至关重要的一步。无论你是新手还是有经验的从业者,掌握数据标注的技术细节和常见问题的解决方案都能为你的AI项目增添不少价值。在电信运营商的客服系统中,工单数据是客户问题和解决方案的重要记录。通过对这些工单数据进行有效标注,不仅能够帮助提升客服自动化系统的智能化水平,还能优化客户服务流程,提高客户满意度。本文将详细介绍如何在电信运营商客服工单的背景下进行

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

Andrej Karpathy最新采访:认知核心模型10亿参数就够了,AI会打破教育不公的僵局

夕小瑶科技说 原创  作者 | 海野 AI圈子的红人,AI大神Andrej Karpathy,曾是OpenAI联合创始人之一,特斯拉AI总监。上一次的动态是官宣创办一家名为 Eureka Labs 的人工智能+教育公司 ,宣布将长期致力于AI原生教育。 近日,Andrej Karpathy接受了No Priors(投资博客)的采访,与硅谷知名投资人 Sara Guo 和 Elad G

Retrieval-based-Voice-Conversion-WebUI模型构建指南

一、模型介绍 Retrieval-based-Voice-Conversion-WebUI(简称 RVC)模型是一个基于 VITS(Variational Inference with adversarial learning for end-to-end Text-to-Speech)的简单易用的语音转换框架。 具有以下特点 简单易用:RVC 模型通过简单易用的网页界面,使得用户无需深入了

透彻!驯服大型语言模型(LLMs)的五种方法,及具体方法选择思路

引言 随着时间的发展,大型语言模型不再停留在演示阶段而是逐步面向生产系统的应用,随着人们期望的不断增加,目标也发生了巨大的变化。在短短的几个月的时间里,人们对大模型的认识已经从对其zero-shot能力感到惊讶,转变为考虑改进模型质量、提高模型可用性。 「大语言模型(LLMs)其实就是利用高容量的模型架构(例如Transformer)对海量的、多种多样的数据分布进行建模得到,它包含了大量的先验

图神经网络模型介绍(1)

我们将图神经网络分为基于谱域的模型和基于空域的模型,并按照发展顺序详解每个类别中的重要模型。 1.1基于谱域的图神经网络         谱域上的图卷积在图学习迈向深度学习的发展历程中起到了关键的作用。本节主要介绍三个具有代表性的谱域图神经网络:谱图卷积网络、切比雪夫网络和图卷积网络。 (1)谱图卷积网络 卷积定理:函数卷积的傅里叶变换是函数傅里叶变换的乘积,即F{f*g}

秋招最新大模型算法面试,熬夜都要肝完它

💥大家在面试大模型LLM这个板块的时候,不知道面试完会不会复盘、总结,做笔记的习惯,这份大模型算法岗面试八股笔记也帮助不少人拿到过offer ✨对于面试大模型算法工程师会有一定的帮助,都附有完整答案,熬夜也要看完,祝大家一臂之力 这份《大模型算法工程师面试题》已经上传CSDN,还有完整版的大模型 AI 学习资料,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

【生成模型系列(初级)】嵌入(Embedding)方程——自然语言处理的数学灵魂【通俗理解】

【通俗理解】嵌入(Embedding)方程——自然语言处理的数学灵魂 关键词提炼 #嵌入方程 #自然语言处理 #词向量 #机器学习 #神经网络 #向量空间模型 #Siri #Google翻译 #AlexNet 第一节:嵌入方程的类比与核心概念【尽可能通俗】 嵌入方程可以被看作是自然语言处理中的“翻译机”,它将文本中的单词或短语转换成计算机能够理解的数学形式,即向量。 正如翻译机将一种语言

怎么让1台电脑共享给7人同时流畅设计

在当今的创意设计与数字内容生产领域,图形工作站以其强大的计算能力、专业的图形处理能力和稳定的系统性能,成为了众多设计师、动画师、视频编辑师等创意工作者的必备工具。 设计团队面临资源有限,比如只有一台高性能电脑时,如何高效地让七人同时流畅地进行设计工作,便成为了一个亟待解决的问题。 一、硬件升级与配置 1.高性能处理器(CPU):选择多核、高线程的处理器,例如Intel的至强系列或AMD的Ry