30. 技术专题-DDD随笔
DDD(domain driven design)领域驱动设计优点:结构简单,部署简单等缺点:耦合;技术栈统一,软件包版本锁定;一崩全崩;升级周期长;无法局部扩容;面向对象的基本特征是“封装性”:把类的内部实现细节封装起来,对外提供可供安全调用的方法,从而让类的使用者无需关心类的内部实现,一个类中核心的元素是数据和行为,数据指的是类的属性或者成员变量,而行为指的是类的方法。而我们设计的User类
文章目录
前言
DDD(domain driven design)领域驱动设计
一、介绍
1. 微服务
单体结构项目
优点:结构简单,部署简单等
缺点:耦合;技术栈统一,软件包版本锁定;一崩全崩;升级周期长;无法局部扩容;
微服务结构项目
优点:耦合性低,易于开发和维护;可以用不同技术栈;可以单独扩容;互相隔离,影响小;部署周期短;
缺点:对运维能力要求高;运行效率会降低;技术要求高,需要处理事务最终一致性等问题。
微服务的误区
在设计不好的微服务架构中,微服务之间的调用关系非常复杂,一个来自客户端的请求甚至要经过七八层的微服务调用,这样糟糕的设计不仅导致系统间耦合严重,而且使得服务器端的处理效率非常低。
在项目初始阶段,需要认真思考,我们是否采用微服务,罗列充足的理由。不建议为了微服务而微服务。
2.DDD
DDD(domain driven design)领域驱动设计
-
应用于微服务架构的方法论。
-
在项目的全生命周期内,所有岗位的人员都基于对业务的相同的理解来开展工作。所有人员站在用户的角度、业务的角度去思考问题,而不是站在技术的角度去思考问题。
-
诞生于2004年,兴起于2014年(微服务元年)。
例如:当我们需要开发一个电力系统的时候,我们可能需要了解电力相关的知识或者说成为一个电力专家,当我们开发一个金融系统的时候,我又要成为一个金融专家等等。成本很高。而且我们只开发系统,也不需要成为每一个领域的专家。
-
DDD晦涩难懂,难以落地。因为DDD是方法论,不是行动指南。
-
“盐少许、油少许“,每个人对DDD的理解和落地都不同,而且没有绝对的对错。
-
如果只学习DDD概念而没有了解如何应用的话,会感觉没有落地;而如果过早关注落地的话,会导致理解片面。
-
正确姿势:“从理论到实践,从实践再到理论……”
顺序:把概念的讲解和技术落地分开。
-
不要一下子学DDD的整体。不同岗位、不同阶段的人先从自己的角度学习DDD的一部分。
二、核心概念
1. 领域
领域主要指一个组织做的所有的事情,但是一般为了缩小讨论的范围,会划分为多个子领域。
-
“领域”(Domain):一个组织做的事情。子领域。
-
领域的划分(以手机公司为例):
-
核心域:解决项目的核心问题,和组织业务紧密相关。
-
支撑域:解决项目的非核心问题,则具有组织特性,但不具有通用性。
-
通用域:解决通用问题,没有组织特性。
- 领域的不同分类决定了公司的研发重点。
例如:手机公司
手机研发子领域、手机制造子领域解决了手机的核心问题。
销售子领域、财务子领域、售后子领域这些都和手机相关,比如销售手机和销售电脑、销售房子需要掌握的业务知识不一样。属于支撑域。
保洁、保安统称后勤子领域、人事子领域这些领域属于通用域,手机研发公司的后勤和电子厂的后勤做的事情都是一样的。一般这种领域可以外包给第三方。
不同的组织也会导致划分的结果不一样,这里我们就用不同的公司来代替
比如上述说的外包第三方公司,但是对于保洁家政等公司来说,保洁、保安子领域又属于核心域。
总结
手机研发子领域、手机制造子领域为核心域
销售子领域财务子领域、售后子领域为支撑域
保洁、保安统称后勤子领域、人事子领域为通用域
书中销售子领域划分为核心域,但是视频里面没有明确讲销售子领域划分,我个人认为销售子领域为支撑域,如果从利润角度来划分,它又不得不为核心域,毕竟手机造出来卖不出去,没有收入,一切都是空谈
总之,同样的领域由于划分的不同结果也不一样。根据自己的公司来具体划分。
2. 领域模型(Domain Model)
1、对于领域内的对象进行建模,从而抽象出来模型。
2、以银行为例。
银行的柜台业务领域中,就有储户、柜员、账户等领域模型。建模是DDD中非常核心的事情,一旦定义出了领域模型,我们就可以用领域模型驱动项目的开发。
3、我们的项目应该开始于创建领域模型,而不是考虑如何设计数据库和编写代码。使用领域模型,我们可以一直用业务语言去描述和构建系统,而不是使用技术人员的语言。
事务脚本
使用技术人员的语言去描述和实现业务事务。没有太多设计,没有考虑可扩展性、可维护性,流水账地编写代码。
例子:
总结:这样的代码
优点:编写简单,自然,非常符合开发人员的思维方式。
缺点:事务脚本中,本该属于支撑域的权限的概念出现了在核心域的代码中,我们应该通过AOP(aspect-oriented-programming,面向切面编程)等方式把权限校验的代码放到单独的权限校验支撑域中。这段代码的另外一个问题是,对于需求的快速变化的响应是非常糟糕的,比如系统的需要增加一个“取款金额大于5万的需要主管审批”的功能,我们可能需要对代码进行增加一个if判断,,或者系统增加一个取款成功之后短信的通知等等,随着系统的业务膨胀,上述Withdraw方法会增长到成千上万行,代码的可维护性、可扩展性非常差
3. 通用语言、界限上下文
1、“我想要商品可以被删除”→“我想要把删除的还原回来”→“Windows回收站都能”
2、此“用户”非彼“用户”。
3、通用语言:一个拥有确切含义的、没有二义性的语言。
4、通用语言离不开特定的语义环境,只有确定了通用语言所在的边界,才能没有歧义的描述一个业务对象。
为什么使用DDD(领域模式)领域驱动设计(简称 ddd)概念来源于2004年著名建模专家eric evans发表的他最具 - 掘金
领域驱动设计(DDD)在爱奇艺打赏业务的实践领域驱动设计(DDD)在爱奇艺打赏业务的实践 2021-03-02 20:0 - 掘金
CQRS Journey | Microsoft Learn
DSL 领域特定语言 :以上相关博客或者书籍有时间可以自己了解和查阅。
4. 实体、值对象
- 实体(Entity)
(1) “标识符”用来唯一定位一个对象,在数据库中我们一般用表的主键来实现“标识符”。主键和标识符的思考角度不同。
(2) 实体:拥有唯一的标识符,标识符的值不会改变,而对象的其他状态则会经历各种变化。标识符用来跟踪对象状态变化,一个实体的对象无论怎样变化,我们都能通过标识符定位这个对象。
(3) 实体一般的表现形式就是EF Core中的实体类。
- 值对象(Value Object)
(1) 值对象:没有标识符的对象,也有多个属性,依附于某个实体对象而存在。比如“商家”的地理位置、衣服的RGB颜色。
(2) 定义为值对象和普通属性的区别:体现整体关系
- 总结:
核心是看是否能自己独立存在
能就是实体,否就是值对象
- 例如
身份证ID不会变 但是年龄、姓名、居住地址、有效期等等都会变化,由于身份证ID没有变,这个人(对象)还是之前的那个人(对象)。
反过来就算年龄、姓名、居住地址、有效期等等都一样,但是身份证号不一样,那也是2个不同的人(对象)
5. 聚合、聚合根
- 聚合(Aggregate)
(1) 目的:高内聚,低耦合。有关系的实体紧密协作,而关系很弱的实体被隔离。
(2) 把关系紧密的实体放到一个聚合中,每个聚合中有一个实体作为聚合根(Aggregate Root),所有对于聚合内对象的访问都通过聚合根来进行,外部对象只能持有对聚合根的引用。
(3) 聚合根不仅仅是实体,还是所在聚合的管理者。
- 聚合的意义
1、为什么聚合可以实现“高内聚,低耦合”。
2、聚合体现的是现实世界中整体和部分的关系,比如订单与订单明细。整体封装了对部分的操作,部分与整体有相同的生命周期。部分不会单独与外部系统单独交互,与外部系统的交互都由整体来负责。
好处就是聚合内部的实体可以紧密的工作,聚合之间可以低耦合的工作
- 聚合的划分很难
1、系统中很多实体都存在着不同程度的关系,这些关系到底是设计为聚合之间的关系还是聚合之内的关系是很难的。
2、聚合的判断标准:实体是否是整体和部分的关系,是否存在着相同的生命周期。
3、订单与订单明细?用户与订单?
- 聚合的划分没有标准答案
1、不同的业务流程也就决定了不同的划分方式。
2、新闻和新闻的评论?
- 例子:
传统的新闻网站可以把新闻和对应的新闻评论设计成一个聚合,但是现在大多数新闻网站都有热评榜,这就导致新闻评论是可以单独与外部系统交互的,这就可以设计成2个聚合了。所以得根据自己的系统来合理的划分。
- 聚合的划分的原则
1、尽量把聚合设计的小一点,一个聚合只包含一个聚合根实体和密不可分的实体,实体中只包含最小数量的属性。
2、小聚合有助于进行微服务的拆分。
聚合宁愿设计的小一点也不要设计的太大。
6. 领域服务、应用服务
- 概念
(1) 聚合中的实体中没有业务逻辑代码,只有对象的创建、对象的初始化、状态管理等个体相关的代码。
(2) 对于聚合内的业务逻辑,我们编写领域服务(Domain Service),而对于跨聚合协作以及聚合与外部系统协作的逻辑,我们编写应用服务(Application Service)。
(3) 应用服务协调多个领域服务、外部系统来完成一个用例。
- 在DDD中,一个典型的用例的处理流程如下
第一步,准备业务操作所需要的数据。
第二步,执行由一个或者多个领域模型做出的业务操作,这些操作会修改实体的状态,或者生成一些操作结果。
第三步,把对实体的改变或者操作结果应用于外部系统。
//第一步,准备业务操作所需要的数据。
MyUser user = new MyUser();
//第二步 我这里是新增用户 如果是修改上面应该准备好数据 这里只需要修改就行
//这里做业务处理
user.UserName = req.UserName;
user.PasswordHash = req.Password;
user.CreationTime = DateTime.UtcNow;
ctx.add(user);
//第三步,把对实体的改变或者操作结果应用于外部系统。
ctx.SaveChanges();
- 职责的划分
(1) 领域模型与外部系统不会发生直接交互,即领域服务不会涉及数据库操作。
(2) 业务逻辑放入领域服务,而与外部系统的交互由应用服务来负责。
(3) 领域服务不是必须的,在一些简单的业务处理中(比如增删改查)是没有领域知识(也就是业务逻辑)的,这种情况下应用服务可以完成所有操作,不需要引入领域服务。这样可以避免过度设计。
- “仓储”(Repository)和“工作单元”(Unit Of Work)
(1) 仓储负责按照要求从数据库中读取数据以及把领域服务修改的数据保存回数据库。
(2) 聚合内的数据操作是关系非常紧密的,我们要保证事务的强一致性,而聚合间的协作是关系不紧密的,因此我们只要保证事务的最终一致性即可。
(3) 聚合内的若干相关联的操作组成一个“工作单元”,这些工作单元要么全部成功,要么全部失败。
继续按照上面的例子可以简单的理解为EF Core是仓储、SaveChanges是工作单元(要么这一波全部成功,要么全部失败)。
- 总结:
因为领域服务不依赖外部系统、不保存状态,所以领域服务比应用服务更容易进行单元测试,这对于提高系统的质量是非常有帮助的。
7. 领域事件、集成事件(十分重要)
- 事务脚本处理“事件”
(1) 当发生某事件的时候,执行某个动作。
(2) 当有人回复了用户的提问的时候,系统就向提问者的邮箱发送通知邮件。事务脚本的实现:
- 代码实现
(1) 代码会随着需求的增加而持续膨胀。比如增加功能“如果用户回复的答案中有涉嫌违法的内容,则先把答案隐藏,并且通知审核人员进行审核”。怎么做?
(2) 代码可扩展性低。比如把“发送邮件”改成“发送短信”,怎么办? “开闭原则”:对扩展开放,对修改关闭。
(3) 容错性差。外部系统并不总是稳定的。
优点:关注点分离;容易扩展;容错性好;
- 两种事件
(1) DDD中的事件分为两种类型:领域事件(Domain Events)和集成事件(Integration Events)。
(2) 领域事件:在同一个微服务内的聚合之间的事件传递。使用进程内的通信机制完成。
(3) 集成事件:跨微服务的事件传递。使用事件总线(EventBus)实现。
- 总结
领域事件由于是在同一个进程内进行的,我们通过进程内的通信机制就可以完成:
集成事件由于需要跨微服务进行通信,我们就要引入事件总线(eventbus)来实现事件的传递。我们一般使用消息队列服务器中的“发布/订阅”模式来实现事件总线。
三、 贫血模型与充血模型
1. 概念
-
贫血模型:一个类中只有属性或者成员变量,没有方法。
-
充血模型:一个类中既有属性、成员变量,也有方法。
2. 实践
需求:定义一个类保存用户的用户名、密码、积分;用户必须具有用户名;为了保安全,密码采用密码的散列值保存;用户的初始积分为10分;每次登录成功奖励5个积分,每次登录失败扣3个积分。
3. 总结
面向对象的基本特征是“封装性”:把类的内部实现细节封装起来,对外提供可供安全调用的方法,从而让类的使用者无需关心类的内部实现,一个类中核心的元素是数据和行为,数据指的是类的属性或者成员变量,而行为指的是类的方法。而我们设计的User类只包含数据,不包含行为,我们用心设计的类只能利用面向对象编程的一部分能力。
无论是贫血模型还是充血模型,只不过是逻辑代码放置的位置不一样而已,本质上没有什么区别。这样的观点是错误的。首先,从代码的角度来讲,把本应该属于User类的行为封装到User类中,这是符合单一职责原则的,当系统中其他地方需要调用User类的时候就可以复用User类中的方法了,其次,贫血模型是站在开发人员角度思考问题的,而充血模型是站在业务角度思考问题的。领域专家不明白什么是 “把用户输入的密码进行哈希运算,然后把哈希值保存起来”,但是他们明白“修改密码,检查密码成功”等充血模型反应出来的概念,因此领域模型中的所有行为都应该有业务价值,而不应该只有反映数据属性。
尽管充血模型带来的好处是明显的,但是贫血模型依旧很流行,其根本原因在于早期的很多持久性框架(比如ORM等)要求实体类的所有属性必须是可读可写,而我们可以很简单的把数据库中的表按照字段逐个映射为一个贫血模型的POCO类,这样“数据库驱动”的思维方式更简单直接,因此我们就见到“到处都是贫血模型”的情况了。
值得欣慰的是,目前大部分主流持久性框架都已经支持充血模型的写法了,比如EF Core对充血模型的支持就非常好,因此我们没有理由再继续写贫血模型了。采用充血模型编写代码,我们能更好的实现DDD和模型驱动编程了。
本文的引用仅限自我学习如有侵权,请联系作者删除。
参考知识
DDD
DDD领域驱动设计详解
更多推荐
所有评论(0)