mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-17 08:46:40 +08:00
1379 lines
54 KiB
HTML
1379 lines
54 KiB
HTML
<!DOCTYPE html>
|
||
|
||
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
|
||
|
||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||
|
||
<head>
|
||
|
||
<head>
|
||
|
||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
|
||
|
||
<link rel="icon" href="/static/favicon.png">
|
||
|
||
<title>089 领域模型与数据模型.md.html</title>
|
||
|
||
<!-- Spectre.css framework -->
|
||
|
||
<link rel="stylesheet" href="/static/index.css">
|
||
|
||
<!-- theme css & js -->
|
||
|
||
<meta name="generator" content="Hexo 4.2.0">
|
||
|
||
</head>
|
||
<body>
|
||
<div class="book-container">
|
||
|
||
<div class="book-sidebar">
|
||
|
||
<div class="book-brand">
|
||
|
||
<a href="/">
|
||
|
||
<img src="/static/favicon.png">
|
||
|
||
<span>技术文章摘抄</span>
|
||
|
||
</a>
|
||
|
||
</div>
|
||
|
||
<div class="book-menu uncollapsible">
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li><a href="/" class="current-tab">首页</a></li>
|
||
|
||
</ul>
|
||
<ul class="uncollapsible">
|
||
|
||
<li><a href="../">上一级</a></li>
|
||
|
||
</ul>
|
||
<ul class="uncollapsible">
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/001 「战略篇」访谈 DDD 和微服务是什么关系?.md.html">001 「战略篇」访谈 DDD 和微服务是什么关系?.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/002 「战略篇」开篇词:领域驱动设计,重焕青春的设计经典.md.html">002 「战略篇」开篇词:领域驱动设计,重焕青春的设计经典.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/003 领域驱动设计概览.md.html">003 领域驱动设计概览.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/004 深入分析软件的复杂度.md.html">004 深入分析软件的复杂度.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/005 控制软件复杂度的原则.md.html">005 控制软件复杂度的原则.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/006 领域驱动设计对软件复杂度的应对(上).md.html">006 领域驱动设计对软件复杂度的应对(上).md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/007 领域驱动设计对软件复杂度的应对(下).md.html">007 领域驱动设计对软件复杂度的应对(下).md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/008 软件开发团队的沟通与协作.md.html">008 软件开发团队的沟通与协作.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/009 运用领域场景分析提炼领域知识(上).md.html">009 运用领域场景分析提炼领域知识(上).md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/010 运用领域场景分析提炼领域知识(下).md.html">010 运用领域场景分析提炼领域知识(下).md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/011 建立统一语言.md.html">011 建立统一语言.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/012 理解限界上下文.md.html">012 理解限界上下文.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/013 限界上下文的控制力(上).md.html">013 限界上下文的控制力(上).md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/014 限界上下文的控制力(下).md.html">014 限界上下文的控制力(下).md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/015 识别限界上下文(上).md.html">015 识别限界上下文(上).md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/016 识别限界上下文(下).md.html">016 识别限界上下文(下).md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/017 理解上下文映射.md.html">017 理解上下文映射.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/018 上下文映射的团队协作模式.md.html">018 上下文映射的团队协作模式.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/019 上下文映射的通信集成模式.md.html">019 上下文映射的通信集成模式.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/020 辨别限界上下文的协作关系(上).md.html">020 辨别限界上下文的协作关系(上).md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/021 辨别限界上下文的协作关系(下).md.html">021 辨别限界上下文的协作关系(下).md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/022 认识分层架构.md.html">022 认识分层架构.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/023 分层架构的演化.md.html">023 分层架构的演化.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/024 领域驱动架构的演进.md.html">024 领域驱动架构的演进.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/025 案例 层次的职责与协作关系(图文篇).md.html">025 案例 层次的职责与协作关系(图文篇).md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/026 限界上下文与架构.md.html">026 限界上下文与架构.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/027 限界上下文对架构的影响.md.html">027 限界上下文对架构的影响.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/028 领域驱动设计的代码模型.md.html">028 领域驱动设计的代码模型.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/029 代码模型的架构决策.md.html">029 代码模型的架构决策.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/030 实践 先启阶段的需求分析.md.html">030 实践 先启阶段的需求分析.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/031 实践 先启阶段的领域场景分析(上).md.html">031 实践 先启阶段的领域场景分析(上).md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/032 实践 先启阶段的领域场景分析(下).md.html">032 实践 先启阶段的领域场景分析(下).md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/033 实践 识别限界上下文.md.html">033 实践 识别限界上下文.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/034 实践 确定限界上下文的协作关系.md.html">034 实践 确定限界上下文的协作关系.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/035 实践 EAS 的整体架构.md.html">035 实践 EAS 的整体架构.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/036 「战术篇」访谈:DDD 能帮开发团队提高设计水平吗?.md.html">036 「战术篇」访谈:DDD 能帮开发团队提高设计水平吗?.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/037 「战术篇」开篇词:领域驱动设计的不确定性.md.html">037 「战术篇」开篇词:领域驱动设计的不确定性.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/038 什么是模型.md.html">038 什么是模型.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/039 数据分析模型.md.html">039 数据分析模型.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/040 数据设计模型.md.html">040 数据设计模型.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/041 数据模型与对象模型.md.html">041 数据模型与对象模型.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/042 数据实现模型.md.html">042 数据实现模型.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/043 案例 培训管理系统.md.html">043 案例 培训管理系统.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/044 服务资源模型.md.html">044 服务资源模型.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/045 服务行为模型.md.html">045 服务行为模型.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/046 服务设计模型.md.html">046 服务设计模型.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/047 领域模型驱动设计.md.html">047 领域模型驱动设计.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/048 领域实现模型.md.html">048 领域实现模型.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/049 理解领域模型.md.html">049 理解领域模型.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/050 领域模型与结构范式.md.html">050 领域模型与结构范式.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/051 领域模型与对象范式(上).md.html">051 领域模型与对象范式(上).md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/052 领域模型与对象范式(中).md.html">052 领域模型与对象范式(中).md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/053 领域模型与对象范式(下).md.html">053 领域模型与对象范式(下).md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/054 领域模型与函数范式.md.html">054 领域模型与函数范式.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/055 领域驱动分层架构与对象模型.md.html">055 领域驱动分层架构与对象模型.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/056 统一语言与领域分析模型.md.html">056 统一语言与领域分析模型.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/057 精炼领域分析模型.md.html">057 精炼领域分析模型.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/058 彩色 UML 与彩色建模.md.html">058 彩色 UML 与彩色建模.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/059 四色建模法.md.html">059 四色建模法.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/060 案例 订单核心流程的四色建模.md.html">060 案例 订单核心流程的四色建模.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/061 事件风暴与业务全景探索.md.html">061 事件风暴与业务全景探索.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/062 事件风暴与领域分析建模.md.html">062 事件风暴与领域分析建模.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/063 案例 订单核心流程的事件风暴.md.html">063 案例 订单核心流程的事件风暴.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/064 表达领域设计模型.md.html">064 表达领域设计模型.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/065 实体.md.html">065 实体.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/066 值对象.md.html">066 值对象.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/067 对象图与聚合.md.html">067 对象图与聚合.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/068 聚合设计原则.md.html">068 聚合设计原则.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/069 聚合之间的关系.md.html">069 聚合之间的关系.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/070 聚合的设计过程.md.html">070 聚合的设计过程.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/071 案例 培训领域模型的聚合设计.md.html">071 案例 培训领域模型的聚合设计.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/072 领域模型对象的生命周期-工厂.md.html">072 领域模型对象的生命周期-工厂.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/073 领域模型对象的生命周期-资源库.md.html">073 领域模型对象的生命周期-资源库.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/074 领域服务.md.html">074 领域服务.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/075 案例 领域设计模型的价值.md.html">075 案例 领域设计模型的价值.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/076 应用服务.md.html">076 应用服务.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/077 场景的设计驱动力.md.html">077 场景的设计驱动力.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/078 案例 薪资管理系统的场景驱动设计.md.html">078 案例 薪资管理系统的场景驱动设计.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/079 场景驱动设计与 DCI 模式.md.html">079 场景驱动设计与 DCI 模式.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/080 领域事件.md.html">080 领域事件.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/081 发布者—订阅者模式.md.html">081 发布者—订阅者模式.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/082 事件溯源模式.md.html">082 事件溯源模式.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/083 测试优先的领域实现建模.md.html">083 测试优先的领域实现建模.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/084 深入理解简单设计.md.html">084 深入理解简单设计.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/085 案例 薪资管理系统的测试驱动开发(上).md.html">085 案例 薪资管理系统的测试驱动开发(上).md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/086 案例 薪资管理系统的测试驱动开发(下).md.html">086 案例 薪资管理系统的测试驱动开发(下).md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/087 对象关系映射(上).md.html">087 对象关系映射(上).md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/088 对象关系映射(下).md.html">088 对象关系映射(下).md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
<a class="current-tab" href="/专栏/领域驱动设计实践(完)/089 领域模型与数据模型.md.html">089 领域模型与数据模型.md.html</a>
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/090 领域驱动设计对持久化的影响.md.html">090 领域驱动设计对持久化的影响.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/091 领域驱动设计体系.md.html">091 领域驱动设计体系.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/092 子领域与限界上下文.md.html">092 子领域与限界上下文.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/093 限界上下文的边界与协作.md.html">093 限界上下文的边界与协作.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/094 限界上下文之间的分布式通信.md.html">094 限界上下文之间的分布式通信.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/095 命令查询职责分离.md.html">095 命令查询职责分离.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/096 分布式柔性事务.md.html">096 分布式柔性事务.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/097 设计概念的统一语言.md.html">097 设计概念的统一语言.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/098 模型对象.md.html">098 模型对象.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/099 领域驱动设计参考过程模型.md.html">099 领域驱动设计参考过程模型.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/100 领域驱动设计的精髓.md.html">100 领域驱动设计的精髓.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/101 实践 员工上下文的领域建模.md.html">101 实践 员工上下文的领域建模.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/102 实践 考勤上下文的领域建模.md.html">102 实践 考勤上下文的领域建模.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/103 实践 项目上下文的领域建模.md.html">103 实践 项目上下文的领域建模.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/104 实践 培训上下文的业务需求.md.html">104 实践 培训上下文的业务需求.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/105 实践 培训上下文的领域分析建模.md.html">105 实践 培训上下文的领域分析建模.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/106 实践 培训上下文的领域设计建模.md.html">106 实践 培训上下文的领域设计建模.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/107 实践 培训上下文的领域实现建模.md.html">107 实践 培训上下文的领域实现建模.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/108 实践 EAS 系统的代码模型.md.html">108 实践 EAS 系统的代码模型.md.html</a>
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/109 后记:如何学习领域驱动设计.md.html">109 后记:如何学习领域驱动设计.md.html</a>
|
||
</li>
|
||
|
||
</ul>
|
||
</div>
|
||
|
||
</div>
|
||
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
|
||
|
||
<div class="sidebar-toggle-inner"></div>
|
||
|
||
</div>
|
||
<script>
|
||
|
||
function add_inner() {
|
||
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
|
||
inner.classList.add('show')
|
||
|
||
}
|
||
function remove_inner() {
|
||
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
|
||
inner.classList.remove('show')
|
||
|
||
}
|
||
function sidebar_toggle() {
|
||
|
||
let sidebar_toggle = document.querySelector('.sidebar-toggle')
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let content = document.querySelector('.off-canvas-content')
|
||
|
||
if (sidebar_toggle.classList.contains('extend')) { // show
|
||
|
||
sidebar_toggle.classList.remove('extend')
|
||
|
||
sidebar.classList.remove('hide')
|
||
|
||
content.classList.remove('extend')
|
||
|
||
} else { // hide
|
||
|
||
sidebar_toggle.classList.add('extend')
|
||
|
||
sidebar.classList.add('hide')
|
||
|
||
content.classList.add('extend')
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
function open_sidebar() {
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
|
||
sidebar.classList.add('show')
|
||
|
||
overlay.classList.add('show')
|
||
|
||
}
|
||
|
||
function hide_canvas() {
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
|
||
sidebar.classList.remove('show')
|
||
|
||
overlay.classList.remove('show')
|
||
|
||
}
|
||
</script>
|
||
<div class="off-canvas-content">
|
||
|
||
<div class="columns">
|
||
|
||
<div class="column col-12 col-lg-12">
|
||
|
||
<div class="book-navbar">
|
||
|
||
<!-- For Responsive Layout -->
|
||
|
||
<header class="navbar">
|
||
|
||
<section class="navbar-section">
|
||
|
||
<a onclick="open_sidebar()">
|
||
|
||
<i class="icon icon-menu"></i>
|
||
|
||
</a>
|
||
|
||
</section>
|
||
|
||
</header>
|
||
|
||
</div>
|
||
|
||
<div class="book-content" style="max-width: 960px; margin: 0 auto;
|
||
|
||
overflow-x: auto;
|
||
|
||
overflow-y: hidden;">
|
||
|
||
<div class="book-post">
|
||
|
||
<p id="tip" align="center"></p>
|
||
|
||
<div><h1>089 领域模型与数据模型</h1>
|
||
|
||
<h2>领域模型与数据模型</h2>
|
||
|
||
<p>领域驱动的设计模型最重要的概念就是聚合,同时,聚合还要受到限界上下文边界的控制。Eric Evans 之所以要引入限界上下文,其中一个重要原因就是因为我们“无法维护一个涵盖整个企业的统一模型”,于是需要限界上下文来“标记出不同模型之间的边界和关系”。当领域模型引入限界上下文与聚合之后,领域模型类与数据表之间就有可能突破类与表之间一一对应的关系。因此,在遵循领域驱动设计原则实现持久化时,需要考虑领域模型与数据模型之间的关系。</p>
|
||
|
||
<h3>领域模型与数据模型的分离</h3>
|
||
|
||
<p>资源库是持久化在领域层的抽象。一个资源库对应一个聚合,因此可以认为<strong>聚合是领域模型中最小的持久化单元</strong>。这是了解领域驱动设计对持久化影响的关键,也是在实现阶段,领域模型驱动设计有别于数据模型驱动设计的核心特征。先有领域模型,后有数据模型,采用领域模型驱动设计的过程,就应以限界上下文为基础,面向聚合进行领域模型设计。在确定了聚合内的各个实体与值对象之后,形成对限界上下文领域模型的细化,然后在实现阶段,再考虑该如何针对每个聚合内的对象进行持久化。</p>
|
||
|
||
<p>仍以薪资管理系统为例,对员工的管理和薪资结算分属两个不同的限界上下文:员工上下文(Employee Context)和薪资上下文(Payroll Context)。员工上下文关注员工基本信息的管理,薪资上下文需要对各种类型的员工进行薪资结算,这就会导致这两个限界上下文的领域模型都会包含 Employee 这个领域概念类。在考虑建立它们的持久化数据模型时,存在两种不同的设计方案:</p>
|
||
|
||
<ul>
|
||
|
||
<li>单库单表:在数据模型中统一建立一张员工表,然后在映射元数据中做好对应的配置。这一方案满足单体架构风格。</li>
|
||
|
||
<li>多库多表:为不同的限界上下文建立不同的数据库,员工模型也映射不同的员工表,之间以共同的员工 ID 关联。这一方案符合微服务架构风格。</li>
|
||
|
||
</ul>
|
||
|
||
<p>无论数据模型采用哪一种设计方案,它们的领域模型包括对聚合内实体与值对象的定义,界定的聚合边界都不应有任何区别,即做到领域模型的设计与持久化机制无关。在领域模型中,受到数据模型影响的应只限于ORM元数据定义。如下图所示的代码结构,应不受数据模型设计方案的影响:</p>
|
||
|
||
<p><img src="assets/a3a3a3b0-0f8c-11ea-b790-ddb23a223761" alt="31d8e84b-2647-4739-a5f6-ad9294ef052f.png" /></p>
|
||
|
||
<p>在领域模型中,员工上下文的 Employee 聚合根实体与薪资上下文的员工聚合根实体通过 EmployeeId 建立关联,薪资上下文中的 HourlyEmployee、SalariedEmployee 与 CommissionedEmployee 三个聚合根实体之间没有任何关系。在设计领域模型时,不应该受到数据模型设计的干扰,但在实现领域模型时,就需要确定数据模型的设计方案,并在选定 ORM 框架的基础上,确定该如何映射领域模型到数据模型的实现方案,并编写代码实现。领域模型与数据模型彼此之间的关系如下图所示:</p>
|
||
|
||
<p><img src="assets/bf77a9b0-0f8c-11ea-b8a0-75aeab1e38cf" alt="76245529.png" /></p>
|
||
|
||
<p>从概念上讲,HourlyEmployee、SalariedEmployee 与 CommissionedEmployee 都是员工,似乎应为其建立以 Employee 为父类的继承体系。然而,若采用领域驱动设计,根据业务能力与领域关注点划分了限界上下文,它们又应该分属不同的限界上下文。如果仍然设计为继承体系,就会导致薪资上下文成为员工上下文的遵奉者。这正是对象范式的领域驱动设计与常规的面向对象设计不同之处,<strong>领域驱动设计在战略和战术层面尤为关注和强调限界上下文与聚合的边界控制力</strong>。这是在运用领域驱动设计进行落地实现时,尤其需要注意的一点。</p>
|
||
|
||
<h3>领域模型</h3>
|
||
|
||
<p>不同的限界上下文有着不同的领域模型,也有着不同的统一语言,因此在定义领域模型的类型时,需要注意区分限界上下文的边界。</p>
|
||
|
||
<p>由于员工上下文专注于对员工信息的管理,因此 Employee 类的定义包含了员工所有的基本属性,部分属性则因为体现了更小的内聚的领域概念,被定义为值对象:</p>
|
||
|
||
<pre><code class="language-java">package top.dddclub.payroll.employeecontext.domain;
|
||
public class Employee extends AbstractEntity<EmployeeId> implements AggregateRoot<Employee> {
|
||
|
||
private EmployeeId employeeId;
|
||
|
||
private String name;
|
||
|
||
private Email email;
|
||
|
||
private EmployeeType employeeType;
|
||
|
||
private Gender gender;
|
||
|
||
private Address address;
|
||
|
||
private Contact contact;
|
||
|
||
private LocalDate onBoardingDate;
|
||
|
||
}
|
||
</code></pre>
|
||
|
||
<p>薪资上下文关心的领域逻辑是计算每种员工的薪资。倘若不同类型员工仅存在薪资计算行为的差异,自然可以引入策略模式,将这一行为分离出来,并抽象为薪资计算的接口。然而,不同类型员工还存在完全不同的属性和对等的行为,钟点工需要提交工作时间卡,月薪雇员需要记录缺勤记录,销售人员需要提交销售凭条,它们之间唯一存在的共性就是 EmployeeId,除此之外,我们还需要维护它们各自的一致性。因此,针对薪资上下文的领域模型,可以为不同类型雇员建立不同的聚合,然后在薪资计算行为层面引入抽象,保持适度的扩展能力:</p>
|
||
|
||
<p><img src="assets/dbe50980-0f8c-11ea-b700-85fa1d9e7b57" alt="73601347.png" /></p>
|
||
|
||
<p>薪资上下文领域模型的聚合定义如下:</p>
|
||
|
||
<pre><code class="language-java">package top.dddclub.payroll.payrollcontext.domain.hourlyemployee;
|
||
public class HourlyEmployee extends AbstractEntity<EmployeeId> implements AggregateRoot<HourlyEmployee> {
|
||
|
||
private EmployeeId employeeId;
|
||
|
||
private Salary salaryOfHour;
|
||
|
||
private List<TimeCard> timeCards = new ArrayList<>();
|
||
public Payroll payroll(Period settlementPeriod) { }
|
||
|
||
}
|
||
package top.dddclub.payroll.payrollcontext.domain.salariedemployee;
|
||
public class SalariedEmployee extends AbstractEntity<EmployeeId> implements AggregateRoot<SalariedEmployee> {
|
||
|
||
private EmployeeId employeeId;
|
||
|
||
private Salary salaryOfMonth;
|
||
|
||
private List<Absence> absences = new ArrayList<>();
|
||
public Payroll payroll(Period settlementPeriod) { }
|
||
|
||
}
|
||
package top.dddclub.payroll.payrollcontext.domain.commissionedemployee;
|
||
public class CommissionedEmployee extends AbstractEntity<EmployeeId> implements AggregateRoot<CommissionedEmployee> {
|
||
|
||
private EmployeeId employeeId;
|
||
|
||
private Salary salaryOfTwoWeeks;
|
||
|
||
private List<Commission> commissions = new ArrayList<>();
|
||
public Payroll payroll(Period settlementPeriod) { }
|
||
|
||
}
|
||
</code></pre>
|
||
|
||
<p>引入聚合的目的在于保证领域概念的完整性,并确保外部的调用者不会因为直接访问聚合边界内除聚合根实体之外的其余对象而破坏这种完整性。例如在 HourlyEmployee 类的 payroll() 方法中,对 timecards 进行了验证,并通过 filterByPeriod() 方法过滤了不符合结算周期的工作时间卡:</p>
|
||
|
||
<pre><code class="language-java">public class HourlyEmployee extends... {
|
||
|
||
public Payroll payroll(Period settlementPeriod) {
|
||
|
||
// 对工作时间卡进行验证
|
||
|
||
if (Objects.isNull(timeCards) || timeCards.isEmpty()) {
|
||
|
||
return new Payroll(this.employeeId, settlementPeriod.beginDate(), settlementPeriod.endDate(), Salary.zero());
|
||
|
||
}
|
||
Salary regularSalary = calculateRegularSalary(settlementPeriod);
|
||
|
||
Salary overtimeSalary = calculateOvertimeSalary(settlementPeriod);
|
||
|
||
Salary totalSalary = regularSalary.add(overtimeSalary);
|
||
return new Payroll(this.employeeId, settlementPeriod.beginDate(), settlementPeriod.endDate(), totalSalary);
|
||
|
||
}
|
||
private Salary calculateRegularSalary(Period period) {
|
||
|
||
int regularHours = filterByPeriod(period)
|
||
|
||
.map(TimeCard::getRegularWorkHours)
|
||
|
||
.reduce(0, Integer::sum);
|
||
|
||
return salaryOfHour.multiply(regularHours);
|
||
|
||
}
|
||
private Salary calculateOvertimeSalary(Period period) {
|
||
|
||
int overtimeHours = filterByPeriod(period)
|
||
|
||
.filter(TimeCard::isOvertime)
|
||
|
||
.map(TimeCard::getOvertimeWorkHours)
|
||
|
||
.reduce(0, Integer::sum);
|
||
return salaryOfHour.multiply(OVERTIME_FACTOR).multiply(overtimeHours);
|
||
|
||
}
|
||
// 过滤不符合结算周期条件的工作时间卡
|
||
|
||
private Stream<TimeCard> filterByPeriod(Period period) {
|
||
|
||
return timeCards.stream()
|
||
|
||
.filter(t -> t.isIn(period));
|
||
|
||
}
|
||
|
||
}
|
||
</code></pre>
|
||
|
||
<p>通过测试驱动开发实现聚合的业务逻辑时,编写的单元测试也应体现这种聚合内的规则约束。例如,当钟点工缺少工作时间卡时,计算的薪资为 0,测试需要体现这一规则约束:</p>
|
||
|
||
<pre><code class="language-java">public class HourlyEmployeeTest {
|
||
|
||
@Test
|
||
|
||
public void should_be_0_given_null_timecard() {
|
||
|
||
//given
|
||
|
||
HourlyEmployee hourlyEmployee = EmployeeFixture.hourlyEmployeeOf(employeeId, null);
|
||
//when
|
||
|
||
Payroll payroll = hourlyEmployee.payroll(settlementPeriod);
|
||
//then
|
||
|
||
assertThat(payroll).isNotNull();
|
||
|
||
assertThat(payroll.employeId().value()).isEqualTo(employeeId);
|
||
|
||
assertThat(payroll.amount()).isEqualTo(Salary.zero());
|
||
|
||
}
|
||
|
||
}
|
||
</code></pre>
|
||
|
||
<p>三个聚合根实体定义的 payroll(Period) 方法在抽象上保持了一致性,不过,由于员工在计算薪资之前还需要调用聚合资源库获取对应的员工对象列表,因此还需要引入领域服务做进一步封装,如 HourlyEmployeePayrollCalculator 服务:</p>
|
||
|
||
<pre><code class="language-java">public class HourlyEmployeePayrollCalculator {
|
||
|
||
private HourlyEmployeeRepository employeeRepository;
|
||
public void setRepository(HourlyEmployeeRepository employeeRepository) {
|
||
|
||
this.employeeRepository = employeeRepository;
|
||
|
||
}
|
||
public List<Payroll> execute(Period settlementPeriod) {
|
||
|
||
List<HourlyEmployee> hourlyEmployees = employeeRepository.allEmployeesOf();
|
||
|
||
return hourlyEmployees.stream()
|
||
|
||
.map(e -> e.payroll(settlementPeriod))
|
||
|
||
.collect(Collectors.toList());
|
||
|
||
}
|
||
|
||
}
|
||
</code></pre>
|
||
|
||
<p>领域服务的 execute() 方法封装了对资源库的调用实现,它的抽象层次方才彻底抹掉了员工类型的差异,可由此引入策略模式:</p>
|
||
|
||
<pre><code class="language-java">public interface PayrollCalculator {
|
||
|
||
List<Payroll> execute(Period settlementPeriod);
|
||
|
||
}
|
||
public class HourlyEmployeePayrollCalculator implements PayrollCalculator {}
|
||
|
||
public class SalariedEmployeePayrollCalculator implements PayrollCalculator {}
|
||
|
||
public class CommissionedEmployeePayrollCalculator implements PayrollCalculator {}
|
||
</code></pre>
|
||
|
||
<p>显然,聚合概念对于以对象范式建立的领域模型存在一定的约束和限制,当然,这种约束与限制也不妨看做是对设计的指导。实际上,倘若我们为钟点工、月薪雇员和销售人员建立了员工继承体系,一旦引入策略模式,就会导致员工类与薪资计算策略类之间存在“平行的继承体系”坏味道。如果没有聚合的要求,又可能会仅定义一个员工类,然后通过员工类型来区分各自的差异,导致产生一个承担了太多职责的过大的类,不利于维护员工与工作时间卡、缺勤记录与销售凭条之间的关系。当然,将它们都设计为一个个仅提供数据属性的贫血对象,自然就不可取了。</p>
|
||
|
||
<h3>数据模型</h3>
|
||
|
||
<p>如前所述,取决于限界上下文的边界以及系统选择的架构风格,存在两种迥然不同的数据模型:单库单表和多库多表。它们的数据模型自然不同,由此也会影响到领域模型到数据模型之间的映射关系。</p>
|
||
|
||
<h4>单库单表的ORM映射</h4>
|
||
|
||
<p>若采用单体架构,员工对应的数据模型最简单的设计就是单库单表,即创建 employees 表。由于员工与工作时间卡、缺勤记录和销售凭条之间都存在一对多关系,因此采用单库设计的数据模型如下所示:</p>
|
||
|
||
<p><img src="assets/037bc5b0-0f8d-11ea-b790-ddb23a223761" alt="74796269.png" /></p>
|
||
|
||
<p>领域驱动设计虽然隔离了领域模型与数据模型,但在实现持久化时,必须为持久化框架提供对象与关系的映射信息。倘若采用 JPA,就是通过 Java 标注来声明,这可以认为是持久化机制对领域模型的一点侵入。由于员工上下文和薪资上下文中的员工领域模型都映射自 employees 表,且薪资上下文的 HourlyEmployee、SalariedEmployee 与 CommissionedEmployee 聚合对应了各自类型的员工数据,故而在领域模型中设置映射信息时,需要作出一点点调整。映射元数据的声明如下所示:</p>
|
||
|
||
<pre><code class="language-java">package top.dddclub.payroll.employeecontext.domain;
|
||
|
||
@Entity
|
||
|
||
@Table(name="employees")
|
||
|
||
public class Employee extends AbstractEntity<EmployeeId> implements AggregateRoot<Employee> {
|
||
|
||
@EmbeddedId
|
||
|
||
private EmployeeId employeeId;
|
||
// 略去其余字段定义
|
||
|
||
}
|
||
package top.dddclub.payroll.payrollcontext.domain.hourlyemployee;
|
||
|
||
@Entity
|
||
|
||
@Table(name = "employees")
|
||
|
||
@DiscriminatorColumn(name = "employeeType", discriminatorType = DiscriminatorType.INTEGER)
|
||
|
||
@DiscriminatorOptions(force=true)
|
||
|
||
@DiscriminatorValue(value = "0")
|
||
|
||
public class HourlyEmployee extends AbstractEntity<EmployeeId> implements AggregateRoot<HourlyEmployee> {
|
||
|
||
@EmbeddedId
|
||
|
||
private EmployeeId employeeId;
|
||
@Embedded
|
||
|
||
private Salary salaryOfHour;
|
||
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
|
||
|
||
@JoinColumn(name = "employeeId", nullable = false)
|
||
|
||
private List<TimeCard> timeCards = new ArrayList<>();
|
||
|
||
}
|
||
package top.dddclub.payroll.payrollcontext.domain.salariedemployee;
|
||
|
||
@Entity
|
||
|
||
@Table(name = "employees")
|
||
|
||
@DiscriminatorColumn(name = "employeeType", discriminatorType = DiscriminatorType.INTEGER)
|
||
|
||
@DiscriminatorOptions(force=true)
|
||
|
||
@DiscriminatorValue(value = "1")
|
||
|
||
public class SalariedEmployee extends AbstractEntity<EmployeeId> implements AggregateRoot<HourlyEmployee> {
|
||
|
||
@EmbeddedId
|
||
|
||
private EmployeeId employeeId;
|
||
@Embedded
|
||
|
||
private Salary salaryOfMonth;
|
||
@ElementCollection
|
||
|
||
@CollectionTable(name = "absences", joinColumns = @JoinColumn(name = "employeeId"))
|
||
|
||
private List<Absence> absences = new ArrayList<>();
|
||
|
||
}
|
||
package top.dddclub.payroll.payrollcontext.domain.commissionedemployee;
|
||
|
||
@Entity
|
||
|
||
@Table(name = "employees")
|
||
|
||
@DiscriminatorColumn(name = "employeeType", discriminatorType = DiscriminatorType.INTEGER)
|
||
|
||
@DiscriminatorOptions(force=true)
|
||
|
||
@DiscriminatorValue(value = "2")
|
||
|
||
public class CommissionedEmployee extends AbstractEntity<EmployeeId> implements AggregateRoot<HourlyEmployee> {
|
||
|
||
@EmbeddedId
|
||
|
||
private EmployeeId employeeId;
|
||
@Embedded
|
||
|
||
private Salary salaryOfTwoWeeks;
|
||
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
|
||
|
||
@JoinColumn(name = "employeeId", nullable = false)
|
||
|
||
private List<Commission> commissions = new ArrayList<>();
|
||
|
||
}
|
||
</code></pre>
|
||
|
||
<p>以上四个聚合类都通过 <code>@Table(name = "employees")</code> 将实体映射到 employees 表。由于这些聚合处于两个不同的限界上下文,各个聚合根实体面对的业务关注点也不相同,故而各自定义的字段存在差别。如 HourlyEmployee 类除了 employeeId 字段外,就仅定义了 salaryOfHour 与 timecards 字段,这两个字段却是 Employee 类所不具备的。站在数据模型的角度看,同一个表对应的领域类存在这样的差异是非常奇怪的,但如果你抛开数据表设计的影响,仅从业务去思考领域类的定义,你又会觉得合情合理。认识到这一差异的根源,有助于理解为何 Eric Evans 要将这一方法体系命名为<strong>领域驱动</strong>设计。</p>
|
||
|
||
<p>无论是 employees 表,还是 Employee 类,都定义了 employeeType 以区分员工的类型。这似乎是 JPA 采用 Single-Table 策略实现继承的标识列。然而如前所述,领域模型中的 HourlyEmployee、SalariedEmployee、CommissionedEmployee 与 Employee 之间并不存在继承关系;但在数据模型中,三种不同类型的员工数据却放在同一张表中,必然需要某种方式告知各自的 Repository:在管理聚合对象的生命周期时,需要对数据加以区分。</p>
|
||
|
||
<p>例如,在调用 HourlyEmployeeRepository 的查询方法时,就只能查询 employeeType 值为 0 的员工数据。要实现这一区分,就可以为 HourlyEmployee 聚合根实体添加 @DiscriminatorColumn 标注来区分它的员工类型。一旦为 HourlyEmployee 等聚合根实体设置了标识列声明,聚合根对应的资源库在查询数据库时,只会返回满足标识列值的数据记录,无需再额外添加对 employeeType 值进行判断的查询条件。反观 Employee 聚合根,由于员工上下文并不需要区分员工类型,它的实体定义反而无需添加标识列声明。</p>
|
||
|
||
<p>钟点工、月薪雇员和销售人员存在薪资结算的业务差异,这是将它们定义为不同聚合的主因。HourlyEmployee 维持了与 TimeCard 的一致性,SalariedEmployee 维持了与 Absence 的一致性,CommissionedEmployee 维持了与 Commission 的一致性,且它们都属于一对多的组合关系,在数据模型中也各自采用了表关联,并以员工 ID 作为从表的外键。</p>
|
||
|
||
<p>TimeCard 和 Commision 被定义为<strong>实体</strong>,因而在各自聚合根实体的定义中,使用了 @OneToMany 标注来映射这种一对多的组合关系;Absence 被定义为<strong>值对象</strong>,因此在 SalariedEmployee 类中使用了 @ElementCollection 标注来表达这种一对多的包含关系。</p>
|
||
|
||
<p>对比薪资上下文中这三个聚合之间的差异,会发现它们虽然都定义了 Salary 类型的字段,但字段名称却并不相同,分别代表日薪、月薪和双周薪。但是,该字段对应的数据列却是完全相同的,在领域模型中通过声明了列名的 Salary 值对象来匹配这种映射关系:</p>
|
||
|
||
<pre><code class="language-java">@Embeddable
|
||
|
||
public class Salary {
|
||
|
||
private static final int SCALE = 2;
|
||
@Column(name = "salary")
|
||
|
||
private BigDecimal value;
|
||
|
||
}
|
||
</code></pre>
|
||
|
||
<h4>多库多表的 ORM 映射</h4>
|
||
|
||
<p>多库多表的数据模型发生了本质的变化,因为要创建的表分布在不同的数据库。由于薪资上下文的数据库不再包含 employees 表,因此需要为钟点工、月薪雇员和销售人员分别创建三张表,以及与之关联的 timecards、absences 和 commissions 表:</p>
|
||
|
||
<p><img src="assets/86f8f750-0f8d-11ea-9b6e-bb22052f4ab5" alt="75352172.png" /></p>
|
||
|
||
<p>如果采用 JPA 规范实现资源库的持久化,就需要在各自的限界上下文中定义 persistence.xml 文件,定义不同的 Persistence Unit,并设置属性指向对应的数据库。</p>
|
||
|
||
<p>既然数据模型中员工相关的表名与结构都发生了变化,领域模型的映射元数据也要做相应的调整:</p>
|
||
|
||
<pre><code class="language-java">package top.dddclub.payroll.employeecontext.domain;
|
||
|
||
@Entity
|
||
|
||
@Table(name="employees")
|
||
|
||
public class Employee extends AbstractEntity<EmployeeId> implements AggregateRoot<Employee> {
|
||
|
||
@EmbeddedId
|
||
|
||
private EmployeeId employeeId;
|
||
// 略去其余字段定义
|
||
|
||
}
|
||
package top.dddclub.payroll.payrollcontext.domain.hourlyemployee;
|
||
|
||
@Entity
|
||
|
||
@Table(name = "hourlyEmployees")
|
||
|
||
public class HourlyEmployee extends AbstractEntity<EmployeeId> implements AggregateRoot<HourlyEmployee> {
|
||
|
||
@EmbeddedId
|
||
|
||
private EmployeeId employeeId;
|
||
@Embedded
|
||
|
||
private Salary salaryOfHour;
|
||
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
|
||
|
||
@JoinColumn(name = "employeeId", nullable = false)
|
||
|
||
private List<TimeCard> timeCards = new ArrayList<>();
|
||
|
||
}
|
||
package top.dddclub.payroll.payrollcontext.domain.salariedemployee;
|
||
|
||
@Entity
|
||
|
||
@Table(name = "salariedEmployees")
|
||
|
||
public class SalariedEmployee extends AbstractEntity<EmployeeId> implements AggregateRoot<HourlyEmployee> {
|
||
|
||
@EmbeddedId
|
||
|
||
private EmployeeId employeeId;
|
||
@Embedded
|
||
|
||
private Salary salaryOfMonth;
|
||
@ElementCollection
|
||
|
||
@CollectionTable(name = "absences", joinColumns = @JoinColumn(name = "employeeId"))
|
||
|
||
private List<Absence> absences = new ArrayList<>();
|
||
|
||
}
|
||
package top.dddclub.payroll.payrollcontext.domain.commissionedemployee;
|
||
|
||
@Entity
|
||
|
||
@Table(name = "commissionedEmployees")
|
||
|
||
public class CommissionedEmployee extends AbstractEntity<EmployeeId> implements AggregateRoot<HourlyEmployee> {
|
||
|
||
@EmbeddedId
|
||
|
||
private EmployeeId employeeId;
|
||
@Embedded
|
||
|
||
private Salary salaryOfTwoWeeks;
|
||
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
|
||
|
||
@JoinColumn(name = "employeeId", nullable = false)
|
||
|
||
private List<Commission> commissions = new ArrayList<>();
|
||
|
||
}
|
||
</code></pre>
|
||
|
||
<p>这些聚合根实体都通过 @Table 指向了对应的数据表。注意,薪资上下文的三个员工聚合根实体不再声明 @DiscriminatorColumn 标注,这是因为每种类型的员工记录在物理上是分离的,每张表仅存储相同类型的员工数据,自然无需定义标识列来区分员工类型。当然,员工上下文的 employees 表仍然定义了 employeeType 列,但它对应的领域模型却不需要列标识。</p>
|
||
|
||
<p>如果去掉所有的 JPA 标注,如上的领域模型与单库单表的领域模型是完全一致的。这也充分说明了领域模型的设计应与数据模型无关。既然领域模型没有差异,聚合对应的资源库,以及领域服务自然也不会有任何差异。换言之,当我们将系统从单体架构迁移到微服务架构时,除了数据库需要进行调整,并考虑可能的数据迁移外,领域层的代码几乎不需要做任何调整。如果在编码时,还注意守护了聚合与限界上下文的边界,保证聚合之间的关联通过聚合 ID 进行(推而广之,限界上下文之间也不应共享领域模型),就可以保证领域层不受架构风格变化的影响,这既符合将业务复杂度与技术复杂度分离的原则,也满足整洁架构的设计思想。</p>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div>
|
||
|
||
<div style="float: left">
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/088 对象关系映射(下).md.html">上一页</a>
|
||
|
||
</div>
|
||
|
||
<div style="float: right">
|
||
|
||
<a href="/专栏/领域驱动设计实践(完)/090 领域驱动设计对持久化的影响.md.html">下一页</a>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||
|
||
</div>
|
||
|
||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997ecd2bb63cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||
|
||
</body>
|
||
|
||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||
|
||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
|
||
|
||
<script>
|
||
|
||
window.dataLayer = window.dataLayer || [];
|
||
function gtag() {
|
||
|
||
dataLayer.push(arguments);
|
||
|
||
}
|
||
gtag('js', new Date());
|
||
|
||
gtag('config', 'G-NPSEEVD756');
|
||
|
||
var path = window.location.pathname
|
||
|
||
var cookie = getCookie("lastPath");
|
||
|
||
console.log(path)
|
||
|
||
if (path.replace("/", "") === "") {
|
||
|
||
if (cookie.replace("/", "") !== "") {
|
||
|
||
console.log(cookie)
|
||
|
||
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
|
||
|
||
}
|
||
|
||
} else {
|
||
|
||
setCookie("lastPath", path)
|
||
|
||
}
|
||
function setCookie(cname, cvalue) {
|
||
|
||
var d = new Date();
|
||
|
||
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
|
||
|
||
var expires = "expires=" + d.toGMTString();
|
||
|
||
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
|
||
|
||
}
|
||
function getCookie(cname) {
|
||
|
||
var name = cname + "=";
|
||
|
||
var ca = document.cookie.split(';');
|
||
|
||
for (var i = 0; i < ca.length; i++) {
|
||
|
||
var c = ca[i].trim();
|
||
|
||
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
|
||
|
||
}
|
||
|
||
return "";
|
||
|
||
}
|
||
</script>
|
||
</html>
|
||
|