领域模型的设计
举一个简单的例子来说明如何进行领域模型设计。
假如我们要为一个小卖店设计一套进销存系统,她为我们提供的业务描述是这样的:每天凌晨从布吉农批市场买苹果、梨、葡萄、橘子、香蕉、荔枝、核桃等等,反正哪些好卖她就买回来卖。葡萄、荔枝不能长久保留,一般要当天卖出去…。
针对上面这段业务描述,我们怎么进行领域模型设计?我给出以下几个步骤来完成领域模型设计。
总结业务描述中的名词
首先建一个名词表,把涉及到的名词列出来:
序号名词备注;
1. 布吉农批市场
2. 买东西的人是一个隐含的名词,每天凌晨从农批市场拿货
3. 苹果
4. 梨
5. 葡萄
6. 橘子
7. 香蕉
8. 荔枝
9. 核桃
10. 顾客是一个隐含的名词,买回来卖的对象
11. 凌晨、当天时间名词,与实体及角色无关
这个名词列表包括了业务的行为主体:角色,以及业务过程中的操作实体:模型,对我们接下来的用例描述、领域模型分析、需求分析很有帮助。当然这个名词列表需要经过进一步分析提炼,成为领域模型
确定业务实体
序号名词描述;
1. 布吉农批市场不是本业务的一个实体
2. 买东西的人是本业务的一个角色
3. 苹果是一个实体
4. 梨是一个实体
5. 葡萄是一个实体
6.橘子是一个实体
7. 香蕉是一个实体
8. 荔枝是一个实体
9. 核桃是一个实体
10. 顾客是本业务的一个角色
11. 凌晨、当天时间名词,与实体及角色无关
当Java世界提供的可选择性框架平台越来越多时 我们可能被平台架构所深深困扰 而无暇顾及软件的真正核心 业务建模 其实 业务领域建模同样是一个比平台架构更复杂 更需要学习的新的领域
相反 在实践中 我们技术人员在经过冗长的平台架构学习和实践后 就匆忙开始项目开发 这时是什么指导他们进行软件业务实现呢?大部分可能是依赖数据库建模 甚至是复杂冗长的数据库存储过程设计 这些已经开始走向面向对象分析设计的反方向 走上了一条错误的软件开发方向 最终开发出缓慢的 经常当机的Java企业系统
如果你没有恰当的OO设计思想 Java就会用性能惩罚你 这可能是Java世界的一个潜规则
那么 一个正确的OOA/OOD/OOP步骤是什么呢?目前围绕模型驱动设计(MDD)的设计思想成为主流思想 MDA更是在MDD基础上提升和升华 下面让我们首先了解 如何使用领域驱动设计思想来分析设计一个软件系统
当我们不再对一个新系统进行数据库提炼时 取而代之的时面向对象的模型提炼 我们必须大刀阔斧地对业务领域进行细分 将一个复杂的业务领域划分为多个小的子领域 同时还必须分清重点和次要部分 抓住核心领域概念 实现重点突破
核心领域模型
精简模型 找出核心领域 将业务需求中最有价值的概念体现出来 让核心变精要 这实际就是一个使复杂问题变简单的过程 也是对我们软件设计人员真正能力的考验
核心领域模型不是轻易能够发现 特别是他处于一个纷乱复杂的众多领域模型结构中时 核心模型通常是我们某个子领域关注的重点 例如订单模型是订单管理领域的核心 消息模型是论坛或消息领域系统的核心
目前 分析领域有很多模式来帮助我们来提炼核心模型 例如四色原型 Martin Fowler 的分析模式等 例如MF的 分析模式 (Analysis Patterns)中的记帐模型就是不仅仅用来记录账目数值 而且可以记录和控制账目的每一次修改 而四色原型则是一种高于分析模式的一种原型基本模式 下面是本人根据四色原型提炼的核心领域模型概念
一般情况下 在企业应用中 核心模型总是在其周围围绕一些所谓的 卫星 这实际上也是来自四色原型的一个推论 核心模型和其 卫星 的类图如下
根据Eric Evans在其 领域驱动设计 一书中定义 领域模型划分为实体和值对象两种 实体模型是指业务领域中具有独立属性的对象 而值对象则可能是一种Description或状态或规则 只要有实体对象 就可能存在实体的状态 状态跟踪有时成为一个业务领域使用计算机软件的首要跟踪 但是 数据库不是对象状态的唯一表达方式 只是一种存储方式(见状态对象 数据库的替代者)
图中 实体核心对象大部分可能有一种类型 例如核心模型是产品 那么存在产品目录 核心模型是消息 就存在消息类型 核心模型是信息 总存在信息类别 我们总是使用分类方式来管理业务领域的信息 有时 类别甚至复杂到树形结构
核心实体模型有时会有一个 :N关联的子实体 一般可能表达实体的细节 例如 核心模型是订单 那么存在订单条目这样一个细节 一个订单中可能有多个订单条目 如果核心模型是信息 那么存在该信息的多个回复或评论 这样的关联一般存在多个业务领域中
模型界面实现
原来 我们以为分析设计阶段无需了解实现细节 分析人员只要闷头做分析UML图 而无需顾及如何具体实现 其实这是一个误区
Eric Evans在其 领域驱动设计 一书中认为 分析人员负责从领域中收集基本概念 设计则必须指明一组适应编程工具构造的组件 以及这些组件必须能够在目标环境中有效执行 模型驱动设计(Model Driven Design)抛弃了分裂分析模型与设计的做法 使用单一的模型来满足这两方面的要求 因此 对于核心模型必须掌握了解其实现细节
从另外一个方面来说 中国的客户总是从界面设计来表达他们的意图(如果中国客户能够使用Use Case等UML图来表达他们概念真是不可想象) 例如客户会说 我希望有一个界面让我将订单数据输入 然后能够查询符合查询条件的订单 因此 我们的核心模型至少能够顺利地映射到界面实现 相反 这个客户有这样订单界面要求 但是你没有提供一个与之适应的核心实体模型 界面实现将变得复杂 甚至走很多弯路 诞生不少DTO垃圾对象
以JdonFramework框架实现为例子 框架提供了围绕核心模型的新增删除修改查询(CRUD)功能以及批量功能的快速实现 尤其CRUD功能实现前提是必须提炼出核心模型 从而其界面设计流程就能通过配置立即实现 这样一步到位实现领域模型到界面的过渡 可以将我们设计核心模型和客户要求的界面需求能够做到完整的统一
开源JdonFramework下载包中message案例实际就是上述核心模型图的一种实现项目 更复杂的项目可以认为是核心模型的重叠和反复使用(从原理上讲 核心模型是四色原型的体现 而四色原型被认为是大部分企业系统的基本组成元素 见[book][UML][Peter Coad]Java Modeling in Color with UML)
核心模型的选择
实际项目中 会存在多个核心模型的重叠和覆盖使用 主要取决于你的领域关注重点
例如当客户和我们说要做一个旅游网站时 我们必须充分了解需求 它的软件系统重点是哪些功能 如果当他首先说 我需要一个酒店设备的查询系统 因为他的客户对酒店设备非常关注 那么我们可能认为酒店设备是这个领域模型的核心 酒店设备 如果他又进行描述 我需要一个界面 客户在输入酒店资料时 选择多个酒店设备 那么在这样一个关注领域 核心模型实际是酒店 而酒店设备可能成为酒店的一个特征实体属性 甚至是值对象了
以进销存系统为例子 在采购系统中 采购单是一个核心实体模型 而原材料是一种辅助实体模型 在库存系统中 入出库单是一个核心实体模型 原材料或成品代表的是一个库存物品概念模型 当需要库存报表查询输出 可以立即计算出来 或将结果缓存起来 缓存起来的结果其实是库存物品对象的状态 可以使用值对象来实现
核心模型的精练
lishixinzhi/Article/program/Java/gj/201311/27660
环艺
,5261工业设计,服装设计4102,广告设计,
戏剧美术设计
,建筑设计
平面1653设计主要包括:
封面设计
,包装设计,
壁饰
,
插图设计
,招贴海报,
标志设计
,
文字设计
,
陶艺设计
环艺主要包括:室内设计,
公共场所设计
,
展示设计
(只要是规划一个区域的具体样子的都是
环境艺术设计
工业设计主要包括:
产品造型设计
戏剧美术设计主要包括:
舞台灯光
,
舞台设计
,
影视美术设计
等
服装设计主要包括:服装设计,人物化妆,
发型设计
望采纳
## 启迪
领域可以理解为业务,领域专家就是对业务很了解的人。
限界上下文也就是微服务的边界,也可以理解为微服务,一个限界上下文=一个微服务。
个人理解领域驱动设计就是微服务驱动设计,从战略上先进行微服务的划分,从战术上针对某个微服务进行领域模型的设计也就是业务模型的设计。
领域模型包括:
- 实体
- 值对象
- 聚合
- 领域服务
- 领域事件
- 资源库
- 应用服务
## 什么是领域驱动设计?
理解领域驱动设计是什么之前,我们先来理解下什么是领域?
领域可以理解为业务,领域专家就是对业务很了解的人。
领域驱动设计的核心就是和最了解业务的人也就是领域专家一起通过领域建模的方式去设计我们的软件程序。
- 那么领域如何驱动设计?或者说业务如何驱动设计?
传统开发过程我们都是基于面向数据开发,拿到产品原型脑海里想着都是应该创建哪些表和哪些字段才能满足需求。
而领域驱动设计开发过程是让我们基于面向业务开发、面向领域模型开发。
领域模型的核心是通过承载和保存领域知识,并通过模型与代码的映射将这些领域知识保存在程序代码中,
在传统开发中,当业务被转换为一张张数据表时,丢失最多的就是领域知识(领域知识也就是我们在模型中定义的一些业务逻辑行为)。
面向领域模型开发的优点:
- 存储方便,统一使用JSON进行存储。
>例:
>订单领域包含基础信息、商品信息、金额信息、支付信息等包含订单全生命周期的子域,
>对于传统面向数据的开发模式我们需要创建N张表进行存储订单的信息,但是面向领域开发时我们
>可以通过利用nosql数据库(mongo、es等)进行保存整个订单域的信息,提高查询、更新效率,简化代码
- 复用性高,引用某个领域模型,就可以拥有该领域模型的所有行为。
>例:
>基于微服务架构下,某个电商应用需要一个判断某个订单是否是在线支付订单的逻辑时,
>对于传统的开发模式我们需要调用订单中心的服务查询订单信息,然后写一个判断是否在线支付订单的方法。
>如果有多个应用都需要这个逻辑时,每个应该都需要重复写相同的方法。
>但面向领域开发时,只需要引用订单中心的jar包,然后统一调用订单领域内的方法即可。
>这样就实现了业务的高内聚
## DDD可以做什么
DDD主要分为两个部分,战略设计与战术设计
- 战略设计
- 围绕微服务拆分
- 战术设计
- 微服务内部设计
## DDD怎么做
- 战略设计
- 和领域专家一起通过(过往经验、事物联系、事件风暴等)划分【限界上下文】
>限界上下文也就是微服务的边界,也可以理解为微服务。
>一个限界上下文=一个微服务
- 战术设计
- 开发人员通过(领域模型)保存【领域知识】
>领域知识也就是事物(角色)、行为(规则)和关系
>
## DDD领域模型
领域模型包含什么?
- 实体
>具有唯一标识,包含着业务知识的【充血模型】对象,用于对唯一性事物进行建模。
>例:
>```
>public class Order {
>private long orderId
>private OrderAmount amount
>private List item
>}
>```
- 值对象
>生成后即不可变对象,通常作为实体的属性,用于描述领域中的事物的某种特征。
>例:
>```
>public class OrderItem {
>private long orderId
>private String productCode
>private String productName
>}
>```
- 聚合
>将实体和值对象在一致性边界之内组成聚合,使用聚合划分微服务(限界上下文)内部的边界
- 领域服务
>分担实体的功能,承接部分业务逻辑,做一些实体不变处理的业务流程。不是必须的
>主要承接内部领域服务调用和外部微服务调用,及一些聚合业务逻辑处理。
>例:
>```
>@Service
>public class ShoppingcartDomainService {
>private final ShoppingcartRepository shoppingcartRepository
>private final ProductFacade productFacade
>private final UserFacade userFacade
>private final PromotionFacade promotionFacade
>
>
>// 1.查询购物车信息
>ShoppingcartDO entity = shoppingcartRepository.loadShoppingcart(userId)
>
>// 2.调用【用户中心】服务查询用户信息
>User user = userFacade.getUser(userId)
>
>// 3.调用【商品中心】服务查询商品信息
>Product product = productFacade.getProduct(productCode)
>
>// 4.调用【活动中心】服务查询活动信息
>Promotion promotion = promotionFacade.getPromotionByProductCode(productCode)
>
>// 5.创建购物车实体
>Shoppingcart shoppingcart = new Shoppingcart(entity.getId, user, product, promotion)
>
>// 6.购物车按活动分组
>shoppingcart.groupby4Promotion()
>}
>```
>
>
- 领域事件
>表示领域中发生的事情,通过领域事件可以实现本地微服务(限界上下文)内的信息同步,同时也可以实现对外部系统的解耦
- 资源库
>保存聚合的地方,将聚合实例存放在资源库(Repository)中,之后再通过该资源库来获取相同的实例。
>
- 应用服务
>应用服务负责流程编排,它将要实现的功能委托给一个或多个领域服务来实现,
>本身只负责处理业务用例的执行顺序以及结果的拼装同时也可以在应用服务做些权限验证等工作。
>
领域驱动设计强调要建立“领域模型”,描述了这个模型能解决什么问题,有哪些特点,但是并没有给出系统化的建模方法,这给了大家很多的发挥空间。
但是不管社区为领域驱动设计引入的建模方式再五花八门,面向对象分析设计建模依然是当前最系统和成熟的方法。只是区别在于:领域驱动设计不再将模型割裂为分析模型、设计模型和实现模型,而是用一个领域模型贯穿设计和实现,并强调代码与模型要保持一致。
另外领域驱动设计建议采用敏捷的“演进式设计”方法逐步设计、演进和精炼领域模型,这需要相应的团队协作方式(业务专家和软件团队长期协作)和对应的演进式工程能力(流水线、自动化测试、重构、简单设计...)做基础。
案例1:我们的实体需要持久化(存储),所以我们需要提供存储的实现。领域层的repository.save等方法提供了持久化接口约定,对于infrastructure来说,如何实现这个方法的代码,就是技术细节。那么我们如何实现这个过程呢?自然是选择缓存,OSS存或者数据库存。如果选择数据库,则进而需要选择orm框架,配置...,实现repository.save的接口,这些都属于持久化所需的技术细节代码。
案例2:我们的应用需要导出资产包相关的excel形式数据,那么当导出资产包数据时,文件领域模块提供了导出的统一接口,资产领域模块提供了资产包的适配接口,而导出excel的代码需要使用easyExcel或者POI等第三方框架,属于技术细节代码。
案例3: 接案例2,为了实现导出时所需的excel排版格式,排版本身的格式与业务有关,比如在我们的业务场景下,我们导出调解明细(我们项目特定的一个领域模型)的时候,只需要按照常见的导出方式即可,而导出资产明细(我们项目特定的一个领域模型)则需要解析拼接所有的动态数据列,合并显示每条数据不同的动态列,而这一切是由业务决定的。根据业务不同有不同的排版要求这一点体现了资产域需要提供文件域的导出策略,调解域也需要实现文件域的导出策略。这些都属于描述业务信息的约定,而这些约定的具体实现比如怎么把实体的那一个属性映射到excel的哪一行哪一列,则属于技术细节。这种区分方式显性化了业务的概念,同时又将实现放在了基础设施层,提供了一定的解耦性。
说完了infrastructure的技术细节的定义,我们接下来聊几个在采用DDD研发模式下,infrastructure层开发过程中经常会遇到的一些问题及我们的解决方案。
为了让业务逻辑和代码实现解耦,在repository的约定中,我们通常用“save保存”代替我们通常说的“insert(插入)“,”update(更新)”这样的技术术语,以屏蔽技术细节。这样带来的一个副作用是,在save时就需要根据策略判断调用insert还是update,我们使用的策略是根据id是否是空决定,即我们所有的实体对象都有一个属性,类型为Id类的子类,id对象的属性(数据库里面实际存放的id值)可能为null,但是id对象,本身不会为null,根据这个对象可以判断当前实体id是否为空。
对于聚合场景,子实体是需要知道聚合根的id的,因为在存储到数据库时可能需要以外键的方式存储对象间的映射关系。
然而,在具体实现中,我们认为,实体之间的对象关系才是标识两个实体之间关系的方式,而不是id,所以生成实体时,先通过对象引用关联对象,表明聚合和实体之间的关系,在保存到数据库的时候,通过实体生成数据库映射类的时候就可以知道当前数据的id是否为空,同时又能知道当前数据之间的关系。
对象之间的关系在1:1聚合保存的时候可能体现不明显,但是当1:N或者N:N批量保存聚合的时候,作用就比较明显了。在我们的系统中发起调解业务就需要批量保存调解批次。代码如下(欢迎吐槽,拥抱进步)
通过这种方式就解决了批量插入不能返回id,同时又能继续复用id.isNew()判断是否为新数据的方式(这里我们没有创建entity基类,所以判断放在了Id上)。
以上方法提供了批量保存时如何区分是新增还是更新。下面我们来谈谈我们项目内提供的插入和更新模板代码。
对于领域来说,save是基本的保存代码。方法传入的参数往往是一个存在于内存中的聚合根对象,有时包含全量的子实体,VO和全量的字段,而在插入场景,对批量请求我们希望支持批量插入,减少对数据库的IO频率,在更新场景下,我们希望减少update时的更新字段的数量(只更新需要更新的字段),这有助于减少数据库IO次数、binlog大小和mysql数据库索引变更带来的开销,所以是非常有必要的。因此对于infrastructure来说,可以提供统一的定制化模板方便repository定制化更新字段的方法快速实现。
由于我们的系统使用的是mybatisplus的ORM方案,所以我们根据api和mysql的批量语句开关提供了一个批量插入和批量更新的Mapper基类,其中insertBatchSomColumn是mybatisplus自带的,updateBatchById则是我们实现的,文档链接如下https://mp.toutiao.com/profile_v4/graphic/preview?pgc_id=7062223527654916621通过这种方式可以轻松地提供定制化更新某几列的sql,减轻sql编写负担。
这一次要讲的其实就是上面提到过的excel导入导出的案例。对于我们的系统来说,具有资产域,文件域,调解域等。其中资产域、调节域等三个域需要导入导出excel。但是我们在设计的时候认为文件的操作属于文件域的概念,所以应当由文件的domain提供功能。但是很明显,具体的导入导出的策略根据数据的不同是可以变化的。所以针对这种情况,我们回归到领域驱动的实现的本质------面向对象技术来思考这个问题的优雅解法。以导入为例
代码如下
上面4份代码是domain的,最下面的是infrastructure的,这里我们只讲infrastructure的(但是我个人认为领域分层后还是需要整体考虑的,所以才会贴上domain的代码)。
这是我们对于跨域业务逻辑的处理办法。
为了保证各领域模型间的解耦,我们经常通过最轻量级的领域事件的方式实现,而不是类似metaq,msgbroker这样的异步分布式消息中间件。领域事件的发送有很多的实现方案,我们倾向于直接使用spring的功能,因为我们需要同步保证事务。但是spring的event发送需要继承ApplicationEvent而领域事件我们又希望独立于spring的event体系,所以我们通过对spring的了解发现了spring已经提供了 PayloadApplicationEvent 可以实现这种功能实现上和其他的spring的event一致,获取我们自己定义的event的方法如下
这里的getPayload()可以获取到我们放进去的领域事件TimeoutEvent
在任何系统中都会有批处理的业务。可能是批处理聚合,可能是批处理聚合内的实体类。这里说一下我之前遇到的一个帖子(jdon)上的讨论。帖子上说的是有一个排班业务,一条班表数据作为聚合存在着每日排班子实体,每日排班下又存在着排班明细子实体,当日期逐渐增加时一条排班需要加载好几年的数据用于生成聚合,而实际上则仅仅只需要计算最近几周的数据。这里存在两点问题
第一点自然不用多说,技术实现以提供业务功能为核心是我一直以来的主张。所以当数据量可能会不断增大的情况下不用加载完整自然是必须的(哪怕内存存储的下也应当尽可能少的消耗)。第二点来说帖子的一位回复者倾向于DomainService提供专门的适配方法,用于加载几周的数据。
我们的系统中存在一个有一些类似的业务。我们的系统需要每隔几分钟就运行一次批处理任务,获取所有已经过期的调解明细,并且设置为过期。调解明细属于调解批次的聚合,所以我们有同样的需求。
我们在此提供一种我们的实现,供参考。
repository的实现根据面向对象原则,仅仅提供如何查询过滤数据库数据
迭代器的实现提供了迭代职责实现
至此实现了批处理加载聚合的逻辑,同时可以提供聚合的部分加载(需要注意业务的正确性不会因为聚合的不完全加载而产生问题)。
最后总结一下
体现你这个系统能在这个场景下干什么
Actor有三种,primary, supporting and offstage
primary一般在左边,是使用service的人(actor)
supporting一般是外部的服务,在右边
offstage用来辅助system完成目标(government tax agency)
符合OO analysis 的定义:Object-oriented analysis: emphases finding and describing objects and related concepts in the problem domain.
domain model需要用 domain model diagram来体现(基于UML)
把这种图吃透,就没啥问题了。其中设计到一些UML的知识点,回顾一下
generalization:继承关系,比如老虎是动物的一种
realization:类与接口的关系,类是接口的所有特征和行为的实现
association:拥有关系,比如老师的学生
aggregation:整体和部分的关系,引擎是车的一部分
compisition:跟aggregation一样,但是比aggregation关系强,比如公司要是没有了,部分一定就没有了
dependancy:使用关系,比如我用(玩)电脑
SSD 跟SD(sequence diagram)是有区别的,SSD把system当作黑盒,不管它内部的东西。SD而主要是以对象之间的交互为主。
而ssd和sd内的元素也不同
在domain model的基础上每个类都增加了function以及relationship之间的名字
参考: https://blog.csdn.net/davidwillo/article/details/73294409