learn.lianglianglee.com/专栏/DDD实战课/16 视图:如何实现服务和数据在微服务各层的协作?.md.html
2022-05-11 19:04:14 +08:00

571 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>16 视图:如何实现服务和数据在微服务各层的协作?.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="/专栏/DDD实战课/00 开篇词 学好了DDD你能做什么.md.html">00 开篇词 学好了DDD你能做什么.md.html</a>
</li>
<li>
<a href="/专栏/DDD实战课/01 领域驱动设计微服务设计为什么要选择DDD.md.html">01 领域驱动设计微服务设计为什么要选择DDD.md.html</a>
</li>
<li>
<a href="/专栏/DDD实战课/02 领域、子域、核心域、通用域和支撑域:傻傻分不清?.md.html">02 领域、子域、核心域、通用域和支撑域:傻傻分不清?.md.html</a>
</li>
<li>
<a href="/专栏/DDD实战课/03 限界上下文:定义领域边界的利器">03 限界上下文:定义领域边界的利器</a>
</li>
<li>
<a href="/专栏/DDD实战课/04 实体和值对象:从领域模型的基础单元看系统设计.md.html">04 实体和值对象:从领域模型的基础单元看系统设计.md.html</a>
</li>
<li>
<a href="/专栏/DDD实战课/05 聚合和聚合根:怎样设计聚合?.md.html">05 聚合和聚合根:怎样设计聚合?.md.html</a>
</li>
<li>
<a href="/专栏/DDD实战课/06 领域事件:解耦微服务的关键.md.html">06 领域事件:解耦微服务的关键.md.html</a>
</li>
<li>
<a href="/专栏/DDD实战课/07 DDD分层架构有效降低层与层之间的依赖.md.html">07 DDD分层架构有效降低层与层之间的依赖.md.html</a>
</li>
<li>
<a href="/专栏/DDD实战课/08 微服务架构模型:几种常见模型的对比和分析.md.html">08 微服务架构模型:几种常见模型的对比和分析.md.html</a>
</li>
<li>
<a href="/专栏/DDD实战课/09 中台:数字转型后到底应该共享什么?.md.html">09 中台:数字转型后到底应该共享什么?.md.html</a>
</li>
<li>
<a href="/专栏/DDD实战课/10 DDD、中台和微服务它们是如何协作的.md.html">10 DDD、中台和微服务它们是如何协作的.md.html</a>
</li>
<li>
<a href="/专栏/DDD实战课/11 DDD实践如何用DDD重构中台业务模型.md.html">11 DDD实践如何用DDD重构中台业务模型.md.html</a>
</li>
<li>
<a href="/专栏/DDD实战课/12 领域建模:如何用事件风暴构建领域模型?.md.html">12 领域建模:如何用事件风暴构建领域模型?.md.html</a>
</li>
<li>
<a href="/专栏/DDD实战课/13 代码模型如何使用DDD设计微服务代码模型.md.html">13 代码模型如何使用DDD设计微服务代码模型.md.html</a>
</li>
<li>
<a href="/专栏/DDD实战课/14 代码模型(下):如何保证领域模型与代码模型的一致性?.md.html">14 代码模型(下):如何保证领域模型与代码模型的一致性?.md.html</a>
</li>
<li>
<a href="/专栏/DDD实战课/15 边界:微服务的各种边界在架构演进中的作用?.md.html">15 边界:微服务的各种边界在架构演进中的作用?.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/DDD实战课/16 视图:如何实现服务和数据在微服务各层的协作?.md.html">16 视图:如何实现服务和数据在微服务各层的协作?.md.html</a>
</li>
<li>
<a href="/专栏/DDD实战课/17 从后端到前端:微服务后,前端如何设计?.md.html">17 从后端到前端:微服务后,前端如何设计?.md.html</a>
</li>
<li>
<a href="/专栏/DDD实战课/18 知识点串讲基于DDD的微服务设计实例.md.html">18 知识点串讲基于DDD的微服务设计实例.md.html</a>
</li>
<li>
<a href="/专栏/DDD实战课/19 总结(一):微服务设计和拆分要坚持哪些原则?.md.html">19 总结(一):微服务设计和拆分要坚持哪些原则?.md.html</a>
</li>
<li>
<a href="/专栏/DDD实战课/20 总结分布式架构关键设计10问.md.html">20 总结分布式架构关键设计10问.md.html</a>
</li>
<li>
<a href="/专栏/DDD实战课/答疑有关3个典型问题的讲解.md.html">答疑有关3个典型问题的讲解.md.html</a>
</li>
<li>
<a href="/专栏/DDD实战课/结束语 所谓高手,就是跨过坑和大海.md.html">结束语 所谓高手,就是跨过坑和大海.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>16 视图:如何实现服务和数据在微服务各层的协作?</h1>
<p>你好,我是欧创新。</p>
<p>在 DDD 分层架构和微服务代码模型里,我们根据领域对象的属性和依赖关系,将领域对象进行分层,定义了与之对应的代码对象和代码目录结构。分层架构确定了微服务的总体架构,微服务内的主要对象有服务和实体等,它们一起协作完成业务逻辑。</p>
<p>那在运行过程中,这些服务和实体在微服务各层是如何协作的呢?今天我们就来解剖一下基于 DDD 分层架构的微服务,看看它的内部结构到底是什么样的。</p>
<h2>服务的协作</h2>
<h3>1. 服务的类型</h3>
<p>我们先来回顾一下分层架构中的服务。按照分层架构设计出来的微服务,其内部有 Facade 服务、应用服务、领域服务和基础服务。各层服务的主要功能和职责如下。</p>
<p>**Facade 服务:**位于用户接口层,包括接口和实现两部分。用于处理用户发送的 Restful 请求和解析用户输入的配置文件等,并将数据传递给应用层。或者在获取到应用层数据后,将 DO 组装成 DTO将数据传输到前端应用。</p>
<p>**应用服务:**位于应用层。用来表述应用和用户行为,负责服务的组合、编排和转发,负责处理业务用例的执行顺序以及结果拼装,对外提供粗粒度的服务。</p>
<p>**领域服务:**位于领域层。领域服务封装核心的业务逻辑,实现需要多个实体协作的核心领域逻辑。它对多个实体或方法的业务逻辑进行组合或编排,或者在严格分层架构中对实体方法进行封装,以领域服务的方式供应用层调用。</p>
<p>**基础服务:**位于基础层。提供基础资源服务(比如数据库、缓存等),实现各层的解耦,降低外部资源变化对业务应用逻辑的影响。基础服务主要为仓储服务,通过依赖倒置提供基础资源服务。领域服务和应用服务都可以调用仓储服务接口,通过仓储服务实现数据持久化。</p>
<h3>2. 服务的调用</h3>
<p>我们看一下下面这张图。微服务的服务调用包括三类主要场景:微服务内跨层服务调用,微服务之间服务调用和领域事件驱动。</p>
<p><img src="assets/e5d025a6fd69d1f2cf2a1af53253abdb.png" alt="img" /></p>
<p><strong>微服务内跨层服务调用</strong></p>
<p>微服务架构下往往采用前后端分离的设计模式,前端应用独立部署。前端应用调用发布在 API 网关上的 Facade 服务Facade 定向到应用服务。应用服务作为服务组织和编排者,它的服务调用有这样两种路径:</p>
<p>第一种是应用服务调用并组装领域服务。此时领域服务会组装实体和实体方法,实现核心领域逻辑。领域服务通过仓储服务获取持久化数据对象,完成实体数据初始化。</p>
<p>第二种是应用服务直接调用仓储服务。这种方式主要针对像缓存、文件等类型的基础层数据访问。这类数据主要是查询操作,没有太多的领域逻辑,不经过领域层,不涉及数据库持久化对象。</p>
<p><strong>微服务之间的服务调用</strong></p>
<p>微服务之间的应用服务可以直接访问,也可以通过 API 网关访问。由于跨微服务操作,在进行数据新增和修改操作时,你需关注分布式事务,保证数据的一致性。</p>
<p><strong>领域事件驱动</strong></p>
<p>领域事件驱动包括微服务内和微服务之间的事件(详见 [第 06 讲]。微服务内通过事件总线EventBus完成聚合之间的异步处理。微服务之间通过消息中间件完成。异步化的领域事件驱动机制是一种间接的服务访问方式。</p>
<p>当应用服务业务逻辑处理完成后,如果发生领域事件,可调用事件发布服务,完成事件发布。</p>
<p>当接收到订阅的主题数据时,事件订阅服务会调用事件处理领域服务,完成进一步的业务操作。</p>
<h3>3. 服务的封装与组合</h3>
<p>我们看一下下面这张图。微服务的服务是从领域层逐级向上封装、组合和暴露的。</p>
<p><img src="assets/2d6a328a9fd8b4b3906bb9f59435ca1d.png" alt="img" /></p>
<p><strong>基础层</strong></p>
<p>基础层的服务形态主要是仓储服务。仓储服务包括接口和实现两部分。仓储接口服务供应用层或者领域层服务调用,仓储实现服务,完成领域对象的持久化或数据初始化。</p>
<p><strong>领域层</strong></p>
<p>领域层实现核心业务逻辑,负责表达领域模型业务概念、业务状态和业务规则。主要的服务形态有实体方法和领域服务。</p>
<p>实体采用充血模型,在实体类内部实现实体相关的所有业务逻辑,实现的形式是实体类中的方法。实体是微服务的原子业务逻辑单元。在设计时我们主要考虑实体自身的属性和业务行为,实现领域模型的核心基础能力。不必过多考虑外部操作和业务流程,这样才能保证领域模型的稳定性。</p>
<p>DDD 提倡富领域模型,尽量将业务逻辑归属到实体对象上,实在无法归属的部分则设计成领域服务。领域服务会对多个实体或实体方法进行组装和编排,实现跨多个实体的复杂核心业务逻辑。</p>
<p>对于严格分层架构,如果单个实体的方法需要对应用层暴露,则需要通过领域服务封装后才能暴露给应用服务。</p>
<p><strong>应用层</strong></p>
<p>应用层用来表述应用和用户行为,负责服务的组合、编排和转发,负责处理业务用例的执行顺序以及结果的拼装,负责不同聚合之间的服务和数据协调,负责微服务之间的事件发布和订阅。</p>
<p>通过应用服务对外暴露微服务的内部功能,这样就可以隐藏领域层核心业务逻辑的复杂性以及内部实现机制。应用层的主要服务形态有:应用服务、事件发布和订阅服务。</p>
<p>应用服务内用于组合和编排的服务,主要来源于领域服务,也可以是外部微服务的应用服务。除了完成服务的组合和编排外,应用服务内还可以完成安全认证、权限校验、初步的数据校验和分布式事务控制等功能。</p>
<p>为了实现微服务内聚合之间的解耦,聚合之间的服务调用和数据交互应通过应用服务来完成。原则上我们应该禁止聚合之间的领域服务直接调用和聚合之间的数据表关联。</p>
<p><strong>用户接口层</strong></p>
<p>用户接口层是前端应用和微服务之间服务访问和数据交换的桥梁。它处理前端发送的 Restful 请求和解析用户输入的配置文件等,将数据传递给应用层。或获取应用服务的数据后,进行数据组装,向前端提供数据服务。主要服务形态是 Facade 服务。</p>
<p>Facade 服务分为接口和实现两个部分。完成服务定向DO 与 DTO 数据的转换和组装,实现前端与应用层数据的转换和交换。</p>
<h3>4. 两种分层架构的服务依赖关系</h3>
<p>现在我们回顾一下 DDD 分层架构,分层架构有一个重要的原则就是:每层只能与位于其下方的层发生耦合。</p>
<p>那根据耦合的紧密程度,分层架构可以分为两种:严格分层架构和松散分层架构。在严格分层架构中,任何层只能与位于其直接下方的层发生依赖。在松散分层架构中,任何层可以与其任意下方的层发生依赖。</p>
<p>下面我们来详细分析和比较一下这两种分层架构。</p>
<p><strong>松散分层架构的服务依赖</strong></p>
<p>我们看一下下面这张图,在松散分层架构中,领域层的实体方法和领域服务可以直接暴露给应用层和用户接口层。松散分层架构的服务依赖关系,无需逐级封装,可以快速暴露给上层。</p>
<p>但它存在一些问题,第一个是容易暴露领域层核心业务的实现逻辑;第二个是当实体方法或领域服务发生服务变更时,由于服务同时被多层服务调用和组合,不容易找出哪些上层服务调用和组合了它,不方便通知到所有的服务调用方。</p>
<p><img src="assets/5e901b4f7fa964b349e4d6f344786ea1.png" alt="img" /></p>
<p>我们再来看一张图,在松散分层架构中,实体 A 的方法在应用层组合后,暴露给用户接口层 aFacade。abDomainService 领域服务直接越过应用层,暴露给用户接口层 abFacade 服务。松散分层架构中任意下层服务都可以暴露给上层服务。</p>
<p><img src="assets/b35d6fed54e26423c0d61de040ab04a0.jpeg" alt="img" /></p>
<p><strong>严格分层架构的服务依赖</strong></p>
<p>我们看一下下面这张图,在严格分层架构中,每一层服务只能向紧邻的上一层提供服务。虽然实体、实体方法和领域服务都在领域层,但实体和实体方法只能暴露给领域服务,领域服务只能暴露给应用服务。</p>
<p>在严格分层架构中,服务如果需要跨层调用,下层服务需要在上层封装后,才可以提供跨层服务。比如实体方法需要向应用服务提供服务,它需要封装成领域服务。</p>
<p>这是因为通过封装你可以避免将核心业务逻辑的实现暴露给外部,将实体和方法封装成领域服务,也可以避免在应用层沉淀过多的本该属于领域层的核心业务逻辑,避免应用层变得臃肿。还有就是当服务发生变更时,由于服务只被紧邻上层的服务调用和组合,你只需要逐级告知紧邻上层就可以了,服务可管理性比松散分层架构要好是一定的。</p>
<p><img src="assets/ab304d69ee174b5e69cb63d79864ca07.png" alt="img" /></p>
<p>我们还是看图A 实体方法需封装成领域服务 aDomainService 才能暴露给应用服务 aAppService。abDomainService 领域服务组合和封装 A 和 B 实体的方法后,暴露给应用服务 abAppService。</p>
<p><img src="assets/348d60eac28c9dbf7d120d1b7159cdf9.png" alt="img" /></p>
<h2>数据对象视图</h2>
<p>在 DDD 中有很多的数据对象,这些对象分布在不同的层里。它们在不同的阶段有不同的形态。你可以再回顾一下 [第 04 讲],这一讲有详细的讲解。</p>
<p>我们先来看一下微服务内有哪些类型的数据对象?它们是如何协作和转换的?</p>
<p>数据持久化对象 PO(Persistent Object),与数据库结构一一映射,是数据持久化过程中的数据载体。</p>
<p>领域对象 DODomain Object微服务运行时的实体是核心业务的载体。</p>
<p>数据传输对象 DTOData Transfer Object用于前端与应用层或者微服务之间的数据组装和传输是应用之间数据传输的载体。</p>
<p>视图对象 VOView Object用于封装展示层指定页面或组件的数据。</p>
<p>我们结合下面这张图,看看微服务各层数据对象的职责和转换过程。</p>
<p><img src="assets/95524b08051fcd181e65f825005a4c73.png" alt="img" /></p>
<p><strong>基础层</strong></p>
<p>基础层的主要对象是 PO 对象。我们需要先建立 DO 和 PO 的映射关系。当 DO 数据需要持久化时,仓储服务会将 DO 转换为 PO 对象,完成数据库持久化操作。当 DO 数据需要初始化时,仓储服务从数据库获取数据形成 PO 对象,并将 PO 转换为 DO完成数据初始化。</p>
<p>大多数情况下 PO 和 DO 是一一对应的。但也有 DO 和 PO 多对多的情况,在 DO 和 PO 数据转换时,需要进行数据重组。</p>
<p><strong>领域层</strong></p>
<p>领域层的主要对象是 DO 对象。DO 是实体和值对象的数据和业务行为载体,承载着基础的核心业务逻辑。通过 DO 和 PO 转换,我们可以完成数据持久化和初始化。</p>
<p><strong>应用层</strong></p>
<p>应用层的主要对象是 DO 对象。如果需要调用其它微服务的应用服务DO 会转换为 DTO完成跨微服务的数据组装和传输。用户接口层先完成 DTO 到 DO 的转换,然后应用服务接收 DO 进行业务处理。如果 DTO 与 DO 是一对多的关系,这时就需要进行 DO 数据重组。</p>
<p><strong>用户接口层</strong></p>
<p>用户接口层会完成 DO 和 DTO 的互转完成微服务与前端应用数据交互及转换。Facade 服务会对多个 DO 对象进行组装,转换为 DTO 对象,向前端应用完成数据转换和传输。</p>
<p><strong>前端应用</strong></p>
<p>前端应用主要是 VO 对象。展现层使用 VO 进行界面展示,通过用户接口层与应用层采用 DTO 对象进行数据交互。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/DDD实战课/15 边界:微服务的各种边界在架构演进中的作用?.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/DDD实战课/17 从后端到前端:微服务后,前端如何设计?.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":"70996efc8dce3d60","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>