DDD的学习(1-181)
贫血领域对象的实例代码![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/b4ae8e17a57f0fbe667dc72fd0007fc5.jpeg)
这段代码看起来很厉害,但实际上我们根本就不知道savecustome的业务场景,因为这里根本就没有写save的逻辑 处理限制 我们很难去找到为什么,
所以最少会有三个问题:
- 业务意图不明显
- 方法的实现本身增加了潜在的复杂性
- consumer领域对象本身就不是对象,而只是一个数据持有器
通用语言和限界上下文同时构成了ddd的两大支柱
通过以上例子 上面的代码可以写成下面这种
通过阅读代码就能理解他的业务意图,还可以只修改consumer的姓名来测试他
接下来看两段代码
在这一段里,是我们平时用的最多的代码,
- 采用的是以数据为中心,客户必须知道如何正确的把参数传进去 传什么?
- 如果客户只修改了sprintid而没有修改status又会发生什么?
- 暴露了backlogitem的数据结构 注意力放在了数据上,也就是b的属性上
- 客户并不需要知道提交的具体细节,实现代码的逻辑恰好能描述业务行为
2.能简单加几行代码就限制了发布项以外的东西不能被提交 setter做不到
领域 子域 限界上下文
- 什么是领域?
我们可以把领域当作一个大的问题域来理解,如果对一个企业来说,那么就是这个企业要做的所有事情。
2. 什么是子域?
子域是相对于领域的一个概念,顾名思义就是领域被细化以后分成了不同的子域。这个应该相对比较好理解,因为我们人习惯性的会将大的东西进行分割,然后逐个击破。比如“分层思想”,“模块化思想”等。
那么子域就是将一个大的问题域拆分成了很多小的问题域。
并且根据子域的重要性以及作用范围将子域分成了:
1、核心域:重要性最强
2、支撑子域:重要性较低,辅助核心域
3、通用子域:给所有域提供辅助
- 什么是限界上下文?
开发软件的目的就是为了解决问题,领域定义了问题域,子域细分了问题域。那么我们需要考虑如何根据这些问题
域来设计解决方案。
我们说的解决方案就是“领域模型”,领域驱动设计即根据问题域来进行建模。
可是我们想一下,如果我们对整个领域建立一个模型是不是太可怕了,如果系统过于庞大,那么这个模型也将非常庞大,牵一发动全身。
既然模型不好建得太庞大,那么我们可以根据细分的子域来建模型。
比较理想的情况是我们根据一个子域单独建立一个模型,也就是一个问题有一个解决方案。这是比较理想化,有时候我们会遇到一个模型能会对应多个子域,或者多个模型对应一个子域的情况。就是说我们可能需要多个解决方案一起来合作把一个问题解决,或者一个解决方案能够解决多个问题。
那,限界上下文在子域和模型这里起到什么作用呢?
我们姑且认为,限界上下文就是将模型对应的子域给框定一个范围,就是说我这个方案要解决的问题包括这些内容。从这个角度,我们可以将领域模型和限界上下文理解为一对一的关系,而通常一个限界上下文将成为一个系统。因此,我们简单地去认为(实际中这个等式并不准确):领域模型 = 限界上下文 = 一个系统
事实上,限界上下文包含的不只是一个领域模型,还有通用语言,领域服务…等不少东西,这里不去讨论这些内容。
限界上下文的例子
- 下面是一条简化的网上书店的需求:
“当用户浏览图书时,应该看到图书的书名、作者以及其他用户对本书的评级和文字评论。”
初步建模:
设想未来业务变化,网站会销售其它商品,这些商品也需要评论,那么因为图书评论是归属图书领域,所以无法复用。
引入限界上下文概念,划分子域:
这样设计后,评论就独立出来了,可以为系统所有允许评论的【资源】进行评论了,比如已购物的订单子项。这样评论子域就可以复用了。
-
领域建模的四个步骤
-
“处理销售”案例的需求
1. 顾客携带购买的商品或服务到达收银台。
2. 收银员开始一次新的销售。
3. 收银员扫码商品或手工输入商品标识。
4. 系统记录卖出去的商品,并显示该商品的描述、价格和累加值。价格可以根据一套定价规则来计算。
收银员重复3~4步,直到结束。
系统显示总金额。
6. 收银员请顾客支付。
7. 顾客选择现金支付或刷卡
7.1 刷卡则到调到银行系统中授权支付。
8. 系统记录完整的销售,并将销售和支付信息发送到外部的记帐系统(进行记帐和提成)和库存系统(更新库存)。
9. 系统打印收据小票。
-
收银员为顾客将商品装袋。
-
顾客带者商品和收据小票离开。
-
步骤一:分析模型(分析需求、候选域)
此购物流程的销售过程是应用层的表现,最终形成销售订单并完成支付是领域层的职责。而支付是销售订单通过外部领域服务完成的订单状态的变更,所以核心在于销售订单。
下面是若干候选域:
Sale(销售项)
Payment(支付)
SalesLineItem(销售项条目)
Register(销售点终端)
Cashier(收银员)
Customer(顾客)
Store(商店)
Product(商品规格说明)
ProductCatalog(商品目录)
Stock(商品库存)
根据需求设计活动图:
- 步骤二:识别模型(划分主要构成)
步骤说明:进一步分析领域模型,识别出核心域、子域、实体、值对象、领域服务以及之间的关联;
核心域:销售订单。系统核心需求。
子域:商品、商品目录、商店、支付、库存、收银员、购物车。
实体:销售、销售子项;
值对象:支付方式、支付金额
领域服务:商品服务、顾客服务、支付服务、库存服务
限界上下文:
-
销售为核心域,本需求围绕这生成销售订单和支付展开。
-
支付虽然是针对销售订单的支付,但是系统可能存在其它支付业务,所以支付应该独立作为子域。
-
商品、商品目录、库存等都是对销售的支撑子域,应该保持独立。
-
商店、收银员等和销售关联程度低,如果有业绩统计,则会关联销售单,方便统计。
-
购物车:购物车的概念和设计方式视同系统的要求。对于一个单纯的销售系统,购物车相当于暂存架,没有持久化的需要,对于有前端商城,则有持久化的需要,需要构造模型。
- 步骤三:构造模型(聚合和聚合根)
步骤说明:
根据前面的识别出来的各类对象找出聚合根和聚合边界,初步构造出模型。可以用包图绘制:
以销售模型形成聚合边界,其中聚合根为销售订单,通过和外部的各类业务关联形成各类子域构成。
- 步骤四:细化模型
步骤说明:
基于前面步骤二和三进行模型的细化工作,并反复迭代,确认模型是否满足需求,限界上下文的设计是否合理,是否有利于复用和扩展。
为何要用
- 传统的三层架构:
表示层(MVC)
业务逻辑层(service、serviceimpl)
数据访问层(dao、daoimpl、orm)
- DDD的经典四层:
表示层(mvc)
应用层
领域层(实体、值对象、聚合(领域模型)仓储接口-依赖倒置、领域服务)
基础设施层(仓储实现、orm)
- 分层原则
以分布式和平行多人开发为基础,每个层应当充分划分自身职责,不做多余的工作。如UI层不能混入业务逻辑控制内容。
以业务领域为核心,其它层依赖业务层。
业务层应当高度封装业务逻辑,对UI层以应用层提供交流方式。
- 什么环境下考虑DDD实现架构?
1、业务逻辑中大型规模
2、应对分布式(需要扩充DTO)
3、应对外部系统(依赖倒置、扩充适配器、防腐层)
4、应对角色多变(需要扩充DCI ,即数据、上下文、交互模式)
- DTO的概念
用于视图层和领域层交互
只有set get,将视图层和领域层对象隔离
- 对象模型的两类
贫血模型 、充血模型
- 领域模型概念
聚合 聚合根 基本责任 交互责任(角色责任)
核心领域、通用子领域、领域前景说明
用工厂完成实体或聚合不能自理的工作。
- 聚合对象通讯
消息依赖、领域事件(用于强一致性以外的功能可考虑使用,例如转账后发送消息,消息不会影响业务结果),业务场景复杂情况下可减低复杂度,并实现最终一致性。另外可以提高分布式性能,松散耦合。
上下文映射图
可以看作是示例上下文,大家在画上下文映射图的时候可以参照一下,后面的大部分概念,也都围绕它展开。
- 上下文映射图(Context Map):可以进行拆分理解,上下文指的就是限界上下文,映射的意思就是关联、联系,就像 ORM 中,对象与关系的映射,图就是把限界上下文之间的关联与联系表现出来,具体的展示就是类似上面的图。
- 合作关系(Partnership):如果两个限界上下文的团队要么一起成功,要么一起失败,要么一起成功,此时他们需要建立起一种合作关系。他们需要一起协调开发计划和集成管理。两个团队应该在接口的演化上进行合作以同时满足两个系统的需求。应该为相互关联的软件功能定制好计划表,这样可以确保这些功能在同一个发布中完成。
- 共享内核(Shared Kernel):对模型和代码的共享将产生一种紧密的依赖性,对于设计来说,这种依赖性可好可坏。我们需要为共享的部分模型指定一个显式的边界,并保持共享内核的小型化。共享内核具有特殊的状态,在没有与另一个团队协商的情况下,这种状态是不可改变的。我们应该引入一种持续集成过程来保证共享内核和通用语言的一致性。
- 客户方-供应方开发(Customer-Supplier Development):当两个团队处于一种上游-下游关系时,上游团队可能独立于下游团队完成开发,此时下游团队的开发可能会受到很大的影响。因此,在上游团队的计划中,我们应该顾及到下游团队的需求。
- 遵奉者(Confoemist):在存在上游-下游的关系的两个团队中,如果上游团队已经没有动力提供下游团队之所需,下游团队便孤立无援了。出于利于他主义,上游团队可能向下游团队做出种种承诺,但是有很大的可能是:这些承诺是无法实现的。下游团队职能盲目的使用上游团队的模型。
- 防腐层(Anticorruption Layer):在集成两个设计良好的限界上下文时,翻译层可能很简单,甚至可能很优雅的实现。但是,当共享内核、合作关系或客户方-供应方关系无法顺利实现时,此时的翻译将变得复杂。对于下游客户来说,你需要根据自己的领域模型创建一个单独的层,该层作为上游系统的委派向你的系统提供功能。防腐层通过已有的接口与其他系统交互,而其他系统只需要做很小的修改,甚至无须修改。在防腐层内部,它在你自己的模型和他方模型之间翻译转换。
- 开放主机服务(Open Host Service):定义一种协议,让你的子系统通过该协议来访问你的服务。你需要讲该协议公开,这样任何想与你集成的人都可以使用该协议。在有新的集成需求时,你应该对协议进行改进或扩展。对于一些特殊的需求,你可以采用一次性的翻译予以处理,这样可以保持协议的简单性和连贯性。
- 发布语言(Published Language):在两个限界上下文之间翻译模型需要一种公用的语言。此时你应该使用一种发布出来的共享语言来完成集成交流。发布语言通常与开放主机服务一起使用。
- 另谋他路(SpeparateWay):在确定需求时,我们应该做到坚决彻底。如果两套功能没有显著的关系,那么它们是可以被完全解耦的。集成总是昂贵的,有时带给你的好处也不大。声明两个限界上下文之间不存在任何关系,这样使得开发者去另外寻找简单的、专门的方法来解决问题。
- 大泥球(Big Ball of Mud):当我们检查已有系统时,经常会发现系统中存在混杂在一起的模型,它们之间的边界是非常模糊的。此时你应该为整个系统绘制一个边界,然后将其归纳在大泥球范围之列。在这个边界之内,不要尝试使用复杂的建模手段来化解问题。同时,这样的系统有可能会向其他系统蔓延,你应该对此保持警觉。
架构
领域模型层(Domain Model Layer):该层的主要职责是展现业务/领域逻辑、业务处理状态,以及实现业务规则,它同时也包含了领域对象的状态信息。这一层是整个应用程序的核心部分,它可以包含下面这些概念和内容:
- 实体(Entities)
- 值对象(Value Objects)
- 领域服务(Domain Services)
- 仓储契约/接口(Repository Contracts/Interfaces)
- 聚合及其工厂(Aggregates and Factories)
- 聚合根(Aggregate Roots)
- 规约对象(Specifications)
六边形架构
所谓的六边形架构,其实是分层架构的扩展,原来的分层架构通常是上下分层的,比如常见的MVC模式,上层是对外的服务接口,下层是对接存储层或者是集成第三方服务,中层是业务逻辑层。我们跳出分层的概念,会发现上面层和下面层其实都是端口+适配器的实现,上面层开放http/tcp端口,采用rest/soap/mq协议等对外提供服务,同时提供对应协议的适配器;下层也是端口+适配器,只不过应用程序这时候变成了调用者,第三方服务或者存储层提供端口和服务,应用程序本身实现适配功能。
基于上述思考,将分层接口中的上层和下层统一起来就变成了六边形架构,基于端口和适配器的实现,示意图如下:
REST!!!
RESTful风格的架构将‘资源’放在第一位,每个‘资源’都有一个URI与之对应,可以将‘资源’看着是ddd中的实体;RESTful采用具有自描述功能的消息实现无状态通信,提高系统的可用性;至于‘资源’的哪些属性可以公开出去,针对‘资源’的操作,RESTful使用HTTP协议的已有方法来实现:GET、PUT、POST和DELETE。
在DDD的实现中,我们可以将对外的服务设计为RESTful风格的服务,将实体/值对象/领域服务作为’资源’对外提供增删改查服务。但是并不建议直接将实体暴露在外,一来实体的某些隐私属性并不能对外暴露,二来某些资源获取场景并不是一个实体就能满足的,因此我们在实际实践过程中,在领域模型上增加了dto这样一个角色,dto可以组合多个实体/值对象的资源对外暴露。
rest与ddd
Marcus是一个农民。他有一个牧场,有4头猪,12只鸡和3头牛。
那么模拟客户端与之交谈,那么我肯定首先询问牧场的状态:“状态?”
Marcus 回答:“有4头猪,12只鸡和3头牛”。
这是最简单的将状态显现的案例,Marcus用语句“有4头猪,12只鸡和3头牛”将他的牧场状态转给了我。
那么如何以REST方式让Marcus加两头牛到它的牧场呢?
我们经常会范的错误是,你会说:“Marcus, 请加两头牛到你的牧场”。
请注意,我们在这里转换了状态吗?没有,我们这里表达的是动词,有面向函数风格,但是这种表述方式其实是RPC( remote procedure call 远程过程调用),这个过程就是:加两头牛到牧场。
Marcus会悲伤地回答: "400错误, Bad Request. 你是什么意思?"
那么让我们以REST方式请求,原来状态是:4头猪,12只鸡和3头牛,增加了两头牛的状态是:4头猪,12只鸡和3头牛。
那我就会说:“Marcus, 4头猪,12只鸡和5头牛, Please ”
Marcus: "正确!".
我: "Marcus, ...那么你现在状态是什么?".
Marcus: " 4头猪,12只鸡和5头牛".
我: "Ahh, 很好"
这才是真正REST。
原文还提到,如果你希望以RPC调用,那么SOAP是一种RPC方式,但是很重量,性能差。
这里,我想补充的是,这个案例让我们明白REST是如何显现状态的,那么在通用需求中,我们如何表达显现状态呢?
也就是说,REST是将什么状态转移显现出来?首先,我们想到的是应该是将业务逻辑的状态显现出来,而DDD领域驱动设计就是分析设计业务逻辑的,那么,推理结果是,REST应该是将领域层聚合根实体的状态显现出来。
首先,我们看看DDD是如何对需求分析设计的,大致步骤如下:
1.找出需求中的上下文边界。
2.根据上下文切分成模块。
3.从每个上下文中划出聚合边界
4.确定每个聚合边界内的聚合根
其中聚合根的状态是业务逻辑状态的核心所在,REST作为一个接口壳,应该透明地将聚合根实体的状态显现给客户端,客户端通过接受用户发出的命令,透过REST来修改聚合根的实体状态,从而达到实现业务功能的结果。如下图:
CQRS
命令与查询职责分离”。
简而言之,CQRS就是平常大家在讲的读写分离,通常读写分离的目的是为了提高查询性能,同时达到读/写的解耦。让DDD和CQRS结合,我们可以分别对读和写建模,查询模型通常是一种非规范化数据模型,它并不反映领域行为,只是用于数据显示;命令模型执行领域行为,且在领域行为执行完成后,想办法通知到查询模型。
那么命令模型如何通知到查询模型呢? 如果查询模型和领域模型共享数据源,则可以省却这一步;如果没有共用数据源,则可以借助于‘消息模式’(Messaging Patterns)通知到查询模型,从而达到最终一致性(Eventual Consistency)。
Martin在blog中指出:CQRS适用于极少数复杂的业务领域,如果不是很适合反而会增加复杂度;另一个适用场景为获取高性能的服务。
实体
1,对具有‘唯一性’的事物进行建模时候,为什么需要考虑使用实体?
为了区分不同的对象,引入实体。
DDD建模的需要
2,如何生成实体的唯一标识?
答:用户提供唯一性值作为程序的输入;程序通过某种算法生成身份标识;程序依赖的持久化存储,如自增主键;从另外一个限界上下文中获取。
4,如何表达实体的角色和职责?
发现对象的角色和职责;一个角色就是一个实体。
通过接口表达类所实现的角色。
5,如何对实体进行验证和持久化?
属性验证,整体对象验证,对象组合验证
DDD中的实体
DDD中要求实体是唯一的且可持续变化的。意思是说在实体的生命周期内,无论其如何变化,其仍旧是同一个实体。唯一性由唯一的身份标识来决定的。可变性也正反映了实体本身的状态和行为。
唯一标识
举个例子,在有双胞胎的家庭里,家人都可以快速分辨开来。这得益于家人对双胞胎性格和外貌的区分。然而邻居却不能,只能通过名字来区分。上小学后,学校里尽然有重名的,这时候就要取外号区分了。上大学后,要坐火车去学校,买票时就要用身份证号来区分了。
针对这个例子,如果我们要抽象出一个User实体,要如何定义其唯一标识呢?其中性格、外貌、昵称、身份证号都可以作为User实体的属性,在某些场景下某个属性就可以对对象进行区分。但为了确保标识的稳定性,我们只能将身份证号设为唯一身份标识。
唯一标识的类型
唯一标识的类型在不同的场景又有不同的要求。主要可以分为有意义和无意义两种。
在一个简单的应用程序里,一个int类型的自增Id就可以作为唯一标识。优点就是占用空间小,查询速度快。
而在一些业务当中,要求唯一标识有意义,通过唯一标识就能识别出一些基本信息,比如支付宝的交易号,其中就包含了日期和用户ID。这种就属于字符串类型的标识,
唯一标识的生成时机
有某些场景下,唯一标识的生成时机也各不相同,主要分为即时生成和延迟生成。即时生成,即在持久化实体之前,先申请唯一标识,再更新到数据库。延迟生成,即在持久化实体之后。
实体 实例
比如说一个系统的使用者就是用户,那么用户实体需要有唯一标识id,账号昵称name,登陆密码password这些基础属性。
public class User(){
private Long id;
private String name;
private String password;
get()...
set()...
}
值对象
通过分析需求,我们不仅需要用户实体,还需要给用户分配角色,那么我们就需要在用户实体中记录角色id。
public class User(){
private Long id;
private String name;
private String password;
private Long rid;
get()...
set()...
}
public class Role(){
private Long id;
private String rname;
get()...
set()...}
但是这时候需求分析出来用户不止一个角色,那么可以用一个中间表来处理。
public class UserAndRole(){
private Long uid;
private Long rid;}
这样做是相当麻烦的,因为每次做相关修改都需要查询三张表。DDD所提倡的方式就是将实体对象当作值的方式来处理。
public class User(){
private Long id;
private String name;
private String password;
private List<Role> role;
get()...
set()...}
传统的值对象简单的按照业务相关性可以划分为三类指po,vo,bo。
- bo(bussiness object) – 业务对象,业务相关性最强,每个对象之间的逻辑关系体现为业务的复杂性和逻辑性
- vo (value object)-- 数据对象,主要用于服务层,对bo进行拆分,使其适用代码逻辑的东西
- po(persistent object)-- 持久化对象,和数据库表对应的对象,用于ORM。
简单来说保存到数据库中的数据有是po,如果需要将性别(0代表男,1代表女)展示在页面上,要么把age这个属性变成男女这两个汉字再传给视图层的对象就是vo,如果是需要对数据进行逻辑处理的实体对象就叫做bo,比如需要大于或者小于。bo和vo并不是一定和po一摸一样,可以根据实际业务有多个vo、多个bo,理解为传值的对象。
实体的作用
实体的作用是什么呢,最主要的作用就是持久化数据,以持久化为基础实体又划分了4大类:
- 失血模型
- 贫血模型
- 充血模型
- 胀血模型
失血模型简单来说,就是只有属性的getter/setter方法的纯数据类。
贫血模型是实体不仅包含了getter/setter,还包含一些简单的逻辑。
充血模型与贫血模型类似,只是取消了持久化逻辑(dao),将其包含在了实体当中。
胀血模型就是除了充血模型所包含的,还将service层的业务放入实体当中。
DDD提倡使用充血模型实体,取消了dao,同时减轻service层的压力。