<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>NotionNext BLOG</title>
        <link>slw.ac.cn/</link>
        <description>这是一个由NotionNext生成的站点</description>
        <lastBuildDate>Tue, 24 Oct 2023 12:52:50 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>zh-CN</language>
        <copyright>All rights reserved 2023, 蓑笠翁</copyright>
        <item>
            <title><![CDATA[【读书笔记】《Go With The Domain》1. 准备项目案例]]></title>
            <link>slw.ac.cn/article/go-with-the-domain-1.html</link>
            <guid>slw.ac.cn/article/go-with-the-domain-1.html</guid>
            <pubDate>Wed, 04 Oct 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[【关键词】整洁架构 DDD CQRS]]></description>
            <content:encoded><![CDATA[<div id="notion-article" class="mx-auto overflow-hidden "><main class="notion light-mode notion-page notion-full-width notion-block-4409a607308146209b71a55578b7426b"><div class="notion-viewport"></div><div class="notion-collection-page-properties"></div><div class="notion-text notion-block-5ea3eafac433459eb485a330abb16f35">该电子书通过实际案例的方式，介绍了在项目中如何使用DDD和CQRS实践整洁架构，并对一些常见反模式的设计提出了改进方法，最终完成一个用 Go 构建的面向业务的应用程序框架。</div><div class="notion-text notion-block-e70e027cc3f4459f8097fd04070a12a1">本书的想法是不要过多关注基础设施和实现细节，但我们需要有一些基础，以便以后可以在此基础上进行构建。在介绍具体的方法之前，需要先设计一个便于举例的模拟项目，本文就是介绍该模拟项目的框架和背景。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-b93531811002484796d777811926c208" data-id="b93531811002484796d777811926c208"><span><div id="b93531811002484796d777811926c208" class="notion-header-anchor"></div><a class="notion-hash-link" href="#b93531811002484796d777811926c208" title="基础设施：使用GoogleCloud托管K8S集群"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">基础设施：使用GoogleCloud托管K8S集群</span></span></h3><div class="notion-text notion-block-37d68adc41e846dd98478eaf11e3bcf5">考虑到使用自建K8S集群部署需要一个专门的DevOps团队，成本较高，因此采用托管K8S集群到的方式更加灵活方便， 你只需要提供容器镜像即可。在此容器内，您可以运行用任何语言编写的应用程序，该应用程序可以使用 HTTP 或 gRPC API 公开端口。也可以在此容器内处理 Pub/Sub 消息。这就是您在基础设施方面所需要的一切。本书创建了一个功能齐全、真实的应用程序，使用 <code class="notion-inline-code">Terraform </code>通过一条命令将此应用程序部署到 Google Cloud。</div><div class="notion-text notion-block-edf03f65885f4528b6112e7f2a655b44">当然，也可以通过<code class="notion-inline-code">docker-compose</code>将应用部署到本地。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-37f4eb89517a4992a9e7eed4ca712145" data-id="37f4eb89517a4992a9e7eed4ca712145"><span><div id="37f4eb89517a4992a9e7eed4ca712145" class="notion-header-anchor"></div><a class="notion-hash-link" href="#37f4eb89517a4992a9e7eed4ca712145" title="基础设施：本地运行"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">基础设施：本地运行</span></span></h3><div class="notion-text notion-block-c2967f2b63bd43f6b159be8d277ef45a">对于由数百个微服务构建的项目来说，实现这一目标要困难得多。幸运的是，我们的项目只有 5 个服务。在 Wild Workouts 中，我们创建了 Docker Compose，并为前端和后端重新加载了实时代码。</div><ul class="notion-list notion-list-disc notion-block-a97d601c16f8419dae98d58a61af1eab"><li>对于前端，我们使用带有 <code class="notion-inline-code">vue-cli-service</code> 服务工具的容器。</li></ul><ul class="notion-list notion-list-disc notion-block-266e46b68464427b93a3d0795dde40cf"><li>对于后端来说，情况稍微复杂一些。在所有容器中，我们运行<code class="notion-inline-code">reflex</code>工具。 <code class="notion-inline-code">reflex </code>侦听任何触发服务重新编译的代码更改。</li></ul><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-7e635d280fce4e7fa959d4984f9c5605" data-id="7e635d280fce4e7fa959d4984f9c5605"><span><div id="7e635d280fce4e7fa959d4984f9c5605" class="notion-header-anchor"></div><a class="notion-hash-link" href="#7e635d280fce4e7fa959d4984f9c5605" title="运行"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">运行</span></span></h3><ol start="1" class="notion-list notion-list-numbered notion-block-8b0b4013dbc14b30b5fd2b9d71fe0701"><li>安装<span class="notion-teal"><code class="notion-inline-code">docker</code></span>和<span class="notion-teal"><code class="notion-inline-code">docker-compose</code></span></li></ol><ol start="2" class="notion-list notion-list-numbered notion-block-8db14da9483e4f24951444bfd8f06179"><li>下载代码：<code class="notion-inline-code">git clone </code><code class="notion-inline-code"><a target="_blank" rel="noopener noreferrer" class="notion-link" href="https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example.git">https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example.git</a></code><code class="notion-inline-code"> &amp;&amp; cd wild-workouts-go-ddd-example</code></li></ol><ol start="3" class="notion-list notion-list-numbered notion-block-138e696b53d64130abe7be033a5672ee"><li>运行：<code class="notion-inline-code">docker-compose up</code></li></ol><ol start="4" class="notion-list notion-list-numbered notion-block-49100464a17242bd871152f51db181d1"><li>访问： <a target="_blank" rel="noopener noreferrer" class="notion-link" href="http://localhost:8080/">http://localhost:8080/</a>。 这里官方也部署了一个可以公开访问的示例，可以直接访问： <a target="_blank" rel="noopener noreferrer" class="notion-link" href="https://threedotslabs-wildworkouts.web.app/">https://threedotslabs-wildworkouts.web.app</a></li></ol><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-a64c863818c142a0a890eb7c7310f280" data-id="a64c863818c142a0a890eb7c7310f280"><span><div id="a64c863818c142a0a890eb7c7310f280" class="notion-header-anchor"></div><a class="notion-hash-link" href="#a64c863818c142a0a890eb7c7310f280" title="示例项目Wild Workouts是做什么的"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">示例项目Wild Workouts是做什么的</span></span></h3><div class="notion-text notion-block-834d0fe8105c41ca84e69ccf429df0ca">很多Go教程中的模式在实际项目中不起作用的情况有多少？可能太频繁了。现实生活并不像教程中那么简单。为了避免这个问题，我们创建了 Wild Workouts 应用程序作为一个功能齐全的项目。只有一开始花足够的时间，才能在后期的实施过程中节省更多的时间。</div><div class="notion-text notion-block-ea919bf0b01342cfb45727d9144a8a58">闲话少说，进入正题。Wild Workouts 是一款面向私人健身教练和参与者的应用程序。</div><ul class="notion-list notion-list-disc notion-block-97e82f052930402ba151e4da94e7d7d8"><li>教练可以在他们有空的时间段制定培训时间表</li></ul><ul class="notion-list notion-list-disc notion-block-2fdd2299e31748338c6017dc133a6aeb"><li>学员可以安排指定日期的培训。</li></ul><ul class="notion-list notion-list-disc notion-block-66f0dc2ae7d74567b78da5ab0e29cb83"><li>其他功能包括：</li><ul class="notion-list notion-list-disc notion-block-66f0dc2ae7d74567b78da5ab0e29cb83"><li> “积分”管理（学员可以安排多少次培训）</li><li>取消 – 如果培训在开始前 24 小时内取消，学员将不会收到返还的积分</li><li>培训重新安排 </li><ul class="notion-list notion-list-disc notion-block-f6d1ef1c9a4c4e9a93e6782157b6600d"><li>如果有人想要在培训开始前 24 小时内重新安排培训，则需要得到第二位参与者（教练或学员）的批准 </li></ul><li>日历视图</li></ul></ul><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-10dadbca3aab4a8c8d58497bb79dc8a1"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:666px;max-width:100%;flex-direction:column"><img style="object-fit:cover" src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Fb769cf6b-c125-41d2-8f10-c232f7a47a38%2F819b46a8-849b-4dff-ae90-54a4fe46543e%2FUntitled.png?table=block&amp;id=10dadbca-3aab-4a8c-8d58-497bb79dc8a1" alt="教练设置时间表" loading="lazy" decoding="async"/><figcaption class="notion-asset-caption">教练设置时间表</figcaption></div></figure><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-7ff11d4fdfce497b90dc879de3628e22"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:645px;max-width:100%;flex-direction:column"><img style="object-fit:cover" src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Fb769cf6b-c125-41d2-8f10-c232f7a47a38%2F2ac9a4e9-25fc-41e8-9c5c-371a074090e2%2FUntitled.png?table=block&amp;id=7ff11d4f-dfce-497b-90dc-879de3628e22" alt="学员选择健身的时间" loading="lazy" decoding="async"/><figcaption class="notion-asset-caption">学员选择健身的时间</figcaption></div></figure><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-b15f3df960734c23b8192cb71bc11815" data-id="b15f3df960734c23b8192cb71bc11815"><span><div id="b15f3df960734c23b8192cb71bc11815" class="notion-header-anchor"></div><a class="notion-hash-link" href="#b15f3df960734c23b8192cb71bc11815" title="前端技术"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">前端技术</span></span></h3><div class="notion-text notion-block-9e954c5871d54cb5b56ee054bcf1afe0">前端和项目设计关系不大，不需要特变关注，这里用到的前端技术有：</div><ul class="notion-list notion-list-disc notion-block-02d97f75376b40b88c86815c0cdce6b0"><li>OpenAPI (Swagger) client。生成后端接口的说明文档的工具，方便JavaScript访问</li></ul><ul class="notion-list notion-list-disc notion-block-6bd757eb59c9455f8645644b21e1a9ef"><li>Bootstrap。前端资源打包工具，创建HTML等</li></ul><ul class="notion-list notion-list-disc notion-block-616cd51553ac4b54892d8dbea6317c05"><li>Vue.js。前端开发框架</li></ul><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-a9981358bdae406cb4f05e99609b2fab" data-id="a9981358bdae406cb4f05e99609b2fab"><span><div id="a9981358bdae406cb4f05e99609b2fab" class="notion-header-anchor"></div><a class="notion-hash-link" href="#a9981358bdae406cb4f05e99609b2fab" title="后端服务"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">后端服务</span></span></h3><ol start="1" class="notion-list notion-list-numbered notion-block-dfb58278399f42c08437cb6e69fefcc1"><li>trainer服务。管理教练的时间表，提供公开的HTTP接口，和内部gRPC接口</li></ol><ol start="2" class="notion-list notion-list-numbered notion-block-12dfef6d06b74b72ba8679442f7ab5d5"><li>trainings服务。管理学员的培训。提供公开的HTTP接口</li></ol><ol start="3" class="notion-list notion-list-numbered notion-block-dc4915011db343d8bddbe63613d89386"><li>users服务。管理积分和用户信息</li></ol><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-3b0024b010a3458c9bdba79eaed3971d" data-id="3b0024b010a3458c9bdba79eaed3971d"><span><div id="3b0024b010a3458c9bdba79eaed3971d" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b0024b010a3458c9bdba79eaed3971d" title="公开HTTP接口"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">公开HTTP接口</span></span></h4><div class="notion-text notion-block-d6794d4cde4a42039516d92287d4b0fa">应用程序执行的大多数操作都是由 HTTP API 触发的。我多次听到 Go 新手问他们应该使用什么框架来创建 HTTP 服务。我始终建议不要在 Go 中使用任何类型的 HTTP 框架。一个简单的路由器，比如 chi15 就足够了。 chi 只为我们提供了轻量级的粘合剂来定义我们的 API 支持哪些 URL 和方法。在底层，它使用 Go 标准库 http 包，因此所有相关工具（如中间件）都是 100% 兼容的。在 Go 中使用任何类型的框架都会增加不必要的复杂性，并将您的项目与该框架耦合在一起，这里需要遵循KISS原则（Keep It Simple And Stupid）。所有服务都以相同的方式运行 HTTP 服务器。</div><div class="notion-text notion-block-4d9b0d4c9ad64d459a11f0dcdbefe633">注：这里我们使用gin框架也是没有问题的。</div><div class="notion-text notion-block-035ec04f0f9f4a5f84e04def4340981b">然后在trainings服务中启动：</div><div class="notion-text notion-block-8dfb8069c47149ef9d51bd93957011bf"><code class="notion-inline-code">RunHTTPServer</code>的入参是createHandler函数，<code class="notion-inline-code">createHandler</code>需要返回<code class="notion-inline-code">http.Handler</code>。在我们的例子中，它是由<span class="notion-teal"><code class="notion-inline-code"> oapi-codegen</code></span> 生成的 <code class="notion-inline-code">HandlerFromMux </code>(在openapi_api.gen.go)。它为我们提供了 OpenAPI 规范中的所有路径和查询参数</div><div class="notion-text notion-block-395f194db5c14eca815ea7f006838ef0">接口定义在trainings.yml：</div><div class="notion-text notion-block-bbf1d998680c4d2cbc68833f7486670b">如果需要修改接口，需要重新生成GO和JavaScript客户端的代码，已经封装在make命令中： <code class="notion-inline-code">make openapi</code>。</div><div class="notion-text notion-block-76ed79390ff84c4799cd6b095f55af53">工具不仅生成路由，还生成了业务功能接口：</div><div class="notion-text notion-block-f5ab0e10d6fe48c5865cb20d9c0dedcd">下面是实现该接口的例子：</div><div class="notion-text notion-block-9930c396b6644bdc81aebadfc2f80958">HTTP 路径并不是 OpenAPI 规范生成的唯一内容。更重要的是，它还为我们提供了响应和请求的模型。在大多数情况下，模型比 API 路径和方法复杂得多。生成它们可以在任何 API 合同更改期间节省时间、解决问题并减少挫败感。</div><div class="notion-text notion-block-f5248f7a0b95418da3187e0a946312e0">举例，定义的数据（trainings.yml）如下：</div><div class="notion-text notion-block-8c3b447fbaa54b908055c1cf5dac0b7f">生成的结构体如下：</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-565af5ef8fcb4bf6b1d1311f26c2a6d4" data-id="565af5ef8fcb4bf6b1d1311f26c2a6d4"><span><div id="565af5ef8fcb4bf6b1d1311f26c2a6d4" class="notion-header-anchor"></div><a class="notion-hash-link" href="#565af5ef8fcb4bf6b1d1311f26c2a6d4" title="云数据库Firestore "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">云数据库Firestore </span></span></h3><div class="notion-text notion-block-b10e769464ab4a148f2917cf34d4f072">如果我们想以最现代、可扩展且真正无服务器的方式构建应用程序，Firestore 是一个自然的选择。我们将开箱即用。他的限制是1 update / second / one document. 但对于独立的文档是并行处理的，实际使用时问题不大。</div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-896db5b9c1324c8388978f1e0ffbed56" data-id="896db5b9c1324c8388978f1e0ffbed56"><span><div id="896db5b9c1324c8388978f1e0ffbed56" class="notion-header-anchor"></div><a class="notion-hash-link" href="#896db5b9c1324c8388978f1e0ffbed56" title="在本地运行 Firestore "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>在本地运行 Firestore </b></span></span></h4><div class="notion-text notion-block-2fd3e1252f704852ba0d828dac774f16">不幸的是 Firestore 模拟器并不完美。我发现有些情况下模拟器与真实版本不100%兼容。我也遇到过一些情况，当我在事务中更新和读取同一个文档时，它导致了死锁。在我看来，这个功能对于本地开发来说已经足够了。另一种选择可能是拥有一个单独的 Google Cloud 项目来进行本地开发。我在这里的偏好是拥有一个真正本地的本地环境，并且不依赖于任何外部服务。它也更容易设置，并且可以在以后的持续集成中使用。自 5 月底以来，Firestore 模拟器提供了 UI。它已添加到 Docker Compose 中，并可从 <a target="_blank" rel="noopener noreferrer" class="notion-link" href="http://localhost:4000/">http://localhost:4000/</a> 获取。</div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-79a2febe366a4a0fa4ff23e8ccd598e4" data-id="79a2febe366a4a0fa4ff23e8ccd598e4"><span><div id="79a2febe366a4a0fa4ff23e8ccd598e4" class="notion-header-anchor"></div><a class="notion-hash-link" href="#79a2febe366a4a0fa4ff23e8ccd598e4" title="使用Firestore"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">使用Firestore</span></span></h4><div class="notion-text notion-block-276231415a09451996dc85f0cdb0f912">除了 Firestore 实现之外，代码在本地和生产环境中的工作方式相同。当我们在本地使用模拟器时，我们需要在.env文件中将 env <code class="notion-inline-code">FIRESTORE_EMULATOR_HOST </code>设置为模拟器（firestore:8787）来运行我们的应用程序</div><div class="notion-text notion-block-640230bf0af24ceaad2b2a9dc0ac6158">使用firestore：</div><div class="notion-text notion-block-74b56e47cf1a4e6db5ae2fca6ba529b7">读出来的数据都是map[string]interface，再转换为需要的数据格式</div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-aa8acacbce964d4884b9748b8b7618fb" data-id="aa8acacbce964d4884b9748b8b7618fb"><span><div id="aa8acacbce964d4884b9748b8b7618fb" class="notion-header-anchor"></div><a class="notion-hash-link" href="#aa8acacbce964d4884b9748b8b7618fb" title="生产环境部署"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">生产环境部署</span></span></h4><div class="notion-text notion-block-7878853256564ea498a4563057a6b514">再输入一些项目信息和账户信息就可以部署了。</div><div class="notion-blank notion-block-3594717347224e6891d3312b18c23e98"> </div><div class="notion-blank notion-block-a8f0c5f1a04b4c5cb352f64d75ef75d5"> </div><div class="notion-blank notion-block-f7b9ff8957a64a758a1c403d7e5a6a80"> </div><div class="notion-blank notion-block-6094a130b2a2499d8ab4ce528a398a36"> </div><div class="notion-blank notion-block-d4a54147aaff4bc09e7bc81d8fb7da6a"> </div><div class="notion-blank notion-block-4f250b296bf34a36bc1d5bd45655dff5"> </div><div class="notion-blank notion-block-d6045456a03644f484177f033a40c081"> </div><div class="notion-blank notion-block-68bcb2ecfdea4b729ec8f0062c9e5727"> </div><div class="notion-blank notion-block-bdb34502d95645c194c81e83b22e55cb"> </div><div class="notion-text notion-block-19a7b8a658fb46bb9cc4310319560133">《Go With The Domain》电子书下载： </div><div class="notion-blank notion-block-d1a41f0951524159a303ec95ace1b34c"> </div></main></div>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[【读书笔记】《Go With The Domain》14. 战略 DDD 简介]]></title>
            <link>slw.ac.cn/article/go-with-the-domain-14.html</link>
            <guid>slw.ac.cn/article/go-with-the-domain-14.html</guid>
            <pubDate>Wed, 04 Oct 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[【关键词】整洁架构 DDD CQRS]]></description>
            <content:encoded><![CDATA[<div id="notion-article" class="mx-auto overflow-hidden "><main class="notion light-mode notion-page notion-full-width notion-block-291c23932faf45cfa75f7fea5a4d0b40"><div class="notion-viewport"></div><div class="notion-collection-page-properties"></div><div class="notion-text notion-block-95d5656817c84c8ba10a697ea0e2f3b9">几年前，我在一家 SaaS 公司工作，该公司可能遇到了所有可能的软件开发问题。代码非常复杂，添加简单的更改可能需要几个月的时间。项目的所有任务和范围均由项目经理单独定义。开发人员不明白他们要解决什么问题。如果不了解客户的期望，许多实现的功能都是无用的。开发团队也无法提出更好的解决方案。</div><div class="notion-text notion-block-71d7d5fb5fdf472cb81db7b42c057198">尽管我们有微服务，但引入一项更改通常需要对大多数服务进行更改。该架构耦合如此紧密，以至于我们无法独立部署这些“微服务”。业务人员不明白为什么添加“一个按钮”可能需要两个月的时间。最终，利益相关者不再信任开发团队。我们都很沮丧。但情况并非毫无希望。</div><div class="notion-text notion-block-8a2a6a7264434743b6f69c3f5805e6fa">我很幸运能够对领域驱动设计有所了解。那时我还远远不是那个领域的专家。但我的知识足够扎实，可以帮助公司最大限度地减少甚至消除所提到的大部分问题。</div><div class="notion-text notion-block-02945ef42c5b45ba91b3c3e465c81fd9">一段时间过去了，这些问题在其他公司并没有消失。即使这些问题的解决方案存在并且不是神秘的知识。人们似乎并没有意识到这一点。也许是因为 GRASP (1997)、SOLID (2000) 或 DDD（领域驱动设计）(2003) 等旧技术经常被遗忘或被认为已经过时？这让我想起了历史上黑暗时代发生的情况，当时古老的知识被遗忘了。同样，我们可以使用旧的想法。它们仍然有效并且可以解决当今的问题，但它们经常被忽视。这就像我们生活在软件的黑暗时代。</div><div class="notion-text notion-block-85710bec21c8427faee1a2ceedcd58d5">另一个相似之处是关注错误的事情。在历史上的黑暗时代，宗教压制了科学。在软件黑暗时代，<b>基础设施正在淘汰重要的软件设计技术</b>。我并不是说宗教不重要。精神生活非常重要，但如果你正遭受饥饿和疾病的折磨，那就不那么重要了。基础设施也是如此。如果您的软件设计很糟糕，那么拥有出色的 Kubernetes 集群和最精美的微服务基础设施对您也无济于事。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-498ace3415b0407abd7d0122a696d825" data-id="498ace3415b0407abd7d0122a696d825"><span><div id="498ace3415b0407abd7d0122a696d825" class="notion-header-anchor"></div><a class="notion-hash-link" href="#498ace3415b0407abd7d0122a696d825" title="软件黑暗时代是一个系统问题 "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">软件黑暗时代是一个系统问题 </span></span></h3><div class="notion-text notion-block-bf12c7c0cb564aa6bde4e5783503b621">软件黑暗时代是一个非常强大的自我延续系统。如果不了解大局，就无法解决系统性问题。<b>系统思维是一种有助于分析此类复杂问题的技术</b>。我使用这种技术来可视化软件黑暗时代。</div><div class="notion-text notion-block-caa368d5769345f4b480c6999bbbc311">你可以看到事物相互加速的一个大循环。如果不干预，问题就会变得越来越大。领域驱动设计如何解决这个问题？</div><div class="notion-text notion-block-13c5a375dd914a2f8fd8a40aadc2022e">与大多数著名的编程大师不同，我们不希望您只相信我们的故事。我们可以弥补一下。幸运的是，我们可以用科学来解释它为什么有效。更准确地说，是基于科学研究的优秀《<b>加速：精益软件科学和 DevOps（</b>Accelerate: The Science of Lean Software and DevOps<b>）</b>》一书。书中描述的研究提到了表现最好和最差团队的特征。最关键的因素之一是松散耦合的架构。</div><div class="notion-text notion-block-8a62bf577598458d878a17ce555ea0aa">如果您认为微服务可以为您提供松散耦合的架构，那您就大错特错了。我多次见过比单体架构耦合程度更高的微服务。这就是为什么我们需要的不仅仅是基础设施解决方案。这就是领域驱动设计（DDD）发挥作用的时候了。<div class="notion-text-children"><div class="notion-text notion-block-6a4283d7bc8b4f7aa249ecd9efc3da57">这一关键的架构属性使团队能够轻松测试和部署单个组件或服务，即使组织及其运营的系统数量不断增长，也就是说，它允许组织随着规模的扩大而提高生产力。 </div><div class="notion-text notion-block-2472740ea367439ba66c8a7cf04d26e7">(...) 如果忽略这些特征，那么采用部署在容器上的最新的精妙微服务架构并不能保证更高的性能。 </div><div class="notion-text notion-block-6ec24c83c40542889ade9b1a8d757dfc">(…) 支持此策略的架构方法包括使用有界上下文和 API 作为将大型域解耦为更小、更松散耦合的单元的方法，以及使用测试替身和虚拟化作为隔离测试服务或组件的方法。 </div><div class="notion-text notion-block-1706673a169448f6a03bcb10dda5a867">()</div></div></div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-7e57c6fdf1b64a948b4eee7d0f43b095" data-id="7e57c6fdf1b64a948b4eee7d0f43b095"><span><div id="7e57c6fdf1b64a948b4eee7d0f43b095" class="notion-header-anchor"></div><a class="notion-hash-link" href="#7e57c6fdf1b64a948b4eee7d0f43b095" title="DDD 不起作用"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">DDD 不起作用</span></span></h3><div class="notion-text notion-block-4e5a5152f8df442bb1fcaa65b2d3ad54"> 也许您知道有人尝试过 DDD，但它对他们不起作用？也许你和一个不太理解它的人一起工作，试图强行使用这些技术，并使一切变得太复杂？</div><div class="notion-text notion-block-a3b7f21061154e018b9f82a5804a3b05">也许你在 Twitter 上看到过一些著名的软件工程师说 DDD 不起作用？也许对你来说，这是一个传说中的圣杯，有人声称为他们工作——但还没人见过它。</div><div class="notion-text notion-block-6b3b3996a17c4b21981a3719c5c033c0">我们不要忘记，我们生活在软件的黑暗时代。上一个时代的想法有一个问题——有些人可能会错过 DDD 的最初点。在 2003 年首次提出 DDD 的背景下，这并不奇怪。</div><div class="notion-text notion-block-31d377f7987846fdafaa6955f1590508">进入 DDD 世界并不容易。许多书籍和文章由于过度简化而忽略了最重要的 DDD 要点。它们也经常用脱离现实的抽象例子来解释。太长、太复杂、难以理解的例子也并不罕见。让我尝试用最简单的方式解释 DDD。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-a8b8de7db11c4b49b550e854fe4e8105" data-id="a8b8de7db11c4b49b550e854fe4e8105"><span><div id="a8b8de7db11c4b49b550e854fe4e8105" class="notion-header-anchor"></div><a class="notion-hash-link" href="#a8b8de7db11c4b49b550e854fe4e8105" title="从黑暗时代到文艺复兴 "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">从黑暗时代到文艺复兴 </span></span></h3><div class="notion-text notion-block-197157db57564638836e9a5205105185">DDD 技术可以分为两个部分。<b>战术和战略模式</b>。战术领域驱动设计模式是关于<b>如何在代码中实现解决方案</b>。战术 DDD 中没有复杂的技术——它完全是关于面向对象编程的良好实践。但在编写代码之前，您需要知道<b>要实现什么</b>。这就是战略 DDD 模式发挥作用的地方。</div><div class="notion-text notion-block-56c6ee3cce0f40cdabd8fb7965028c26">许多描述 DDD 的资料来源大部分时间都在讨论战术模式。有时，他们甚至会跳过战略模式。您可以<b>仅使用战略模式来练习 DDD</b>。在某些项目中，使用战术 DDD 模式甚至是大材小用。不幸的是，大多数人正在做完全相反的事情。<b>他们只使用战术模式，而不使用战略部分</b>。这太可怕了。</div><div class="notion-text notion-block-f5ab66449f6b40ffa549f51574f93986">如果有人问我产品软件工程中是否存在灵丹妙药(银弹)，我只会说一个候选者：<b>领域驱动设计战略模式</b>。战略 DDD 帮助我们得到以下问题的答案：</div><ul class="notion-list notion-list-disc notion-block-6585386dafc14ee5850609c78b8640a1"><li>您正在解决什么问题？</li></ul><ul class="notion-list notion-list-disc notion-block-ad04ef1e32174e409f5cb3de40ff7cb8"><li>您的解决方案能否满足利益相关者和用户的期望？</li></ul><ul class="notion-list notion-list-disc notion-block-0dd04a317eca419cb847449ac9b2adb0"><li>该项目有多复杂？</li></ul><ul class="notion-list notion-list-disc notion-block-46a3d18fcd184420ab3c7a8f18373494"><li>哪些功能不是必需的？</li></ul><ul class="notion-list notion-list-disc notion-block-71afff10f4214ffbaefe6221c6ad567e"><li>如何分离服务以支持长期快速发展？在实施新项目、添加新功能或进行重构时</li></ul><div class="notion-text notion-block-d6f622769ea44b05923df198cf72a320">这些问题至关重要。战略 DDD 模式为我们提供了一种<b>一致且可预测</b>地回答这些问题的方法。一些工程师告诉我他们“只是工程师”。他们不太关心谁使用他们的软件或为什么使用他们的软件。例如，他们只是实现 JIRA 任务——构建在输入上有一些字节、在输出上有一些字节的服务。这种心态会导致工程师和客户之间的巨大脱节，而我们作为工程师正在努力解决客户的问题。如果没有适当的沟通，就很难创建以正确的方式帮助客户的解决方案。这就是最终的目标——不仅仅是处理字节。</div><div class="notion-text notion-block-1edc76916e144f69adac49fcb8c9c88a">在项目的规划阶段花费少量时间是很诱人的。尽快开始编码并尽早完成。当有人有这样的疑问时，我想说“用 5 天的编码时间，可以节省 1 天的计划时间”。未提出的问题不会神奇地消失。</div><div class="notion-text notion-block-11ee979622ba43168c32b926565ae6ea">克服软件黑暗时代的最佳方法是从多个角度对其进行攻击。让我们看看 DDD 模式如何攻击系统。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-45ae6e4ae500402b8f64f9572c729b52" data-id="45ae6e4ae500402b8f64f9572c729b52"><span><div id="45ae6e4ae500402b8f64f9572c729b52" class="notion-header-anchor"></div><a class="notion-hash-link" href="#45ae6e4ae500402b8f64f9572c729b52" title="事件风暴 "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">事件风暴 </span></span></h3><div class="notion-text notion-block-6302474c46d148e282fba0625c9f650a">事件风暴是战略 DDD 模式和一般软件开发的游戏规则改变者。我无法相信为什么它还没有被世界上的每个团队采用。事件风暴是一个研讨会，在此期间有问题的人（通常是开发人员）与有答案的人（通常是利益相关者）会面。在会议期间，他们可以快速探索复杂的业务领域。一开始，您专注于构建基于领域事件（橙色便签）的完整工作流程。事件风暴是一种超级灵活的方法。因此，您可以验证您的解决方案是否满足预期要求。您还可以根据会话的目标探索数据流、潜在问题或用户体验。</div><div class="notion-text notion-block-2c6152ccca1c4be497e711fa1ccc42f8">验证解决方案是否没有缺陷以及是否符合用户的要求需要几分钟的时间。在开发和部署的代码中引入更改和验证想法的成本要高得多。但是更换黑板上的便利贴成本非常低。</div><div class="notion-text notion-block-844c21a3760043f5a322ff44f2260e0e">土木工程师或火箭工程师可以很快看到错误的结果。他们在完成构建过程之前就可以看到明显存在问题。对于软件来说就不那么简单了，因为它不容易被看到。我们的大多数关键决定不会伤害任何人。功能开发和维护的问题不会一天出现。</div><div class="notion-text notion-block-9b4022154e584fbda1a06387d203cc98">当您计划一个大项目或只是一个故事时，事件风暴很有效。这只是您愿意花多少时间的问题。当我们将它用于一个故事时，它可能会持续 10 分钟到几个小时。我们倾向于花费一天到几天的时间来获得更大的功能。课程结束后，您应该得到以下问题的正确答案：</div><ul class="notion-list notion-list-disc notion-block-a966e33ff69d4da395e51f5b4b0ec91c"><li>您试图解决什么问题 – 而不是猜测什么可能对最终用户有用或假设“我们更了解一切”</li></ul><ul class="notion-list notion-list-disc notion-block-a8c3145dc9e24c1791ab83e9443e5399"><li>利益相关者是否对提议的解决方案感到满意 – 而不是半年后验证这一点实施过程中</li></ul><ul class="notion-list notion-list-disc notion-block-147c36b2264f4694aa383b751384fff3"><li>问题的复杂性显而易见 – 清楚地说明了为什么添加一个按钮可能需要大量工作</li></ul><ul class="notion-list notion-list-disc notion-block-c9f27417cd984d04bb52f9790710414d"><li>如何按职责拆分微服务的初步想法 – 而不是盲目地将“类似事物”分组</li></ul><div class="notion-text notion-block-a5607bf2824648ea80ad6f91088e1d84">最终，您将获得利益相关者更多的信任，因为您正在共同规划解决方案。这是比在地下室进行隔离编码更好的方法。事件风暴的优点在于，<b>正确完成的会话的结果可以直接映射到代码</b>。它应该可以帮助您在开发过程中避免许多讨论并大大加快您的工作速度。</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-753310e9def241f191eb3369db52b37d"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:528px;max-width:100%;flex-direction:column"><img style="object-fit:cover" src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Fb769cf6b-c125-41d2-8f10-c232f7a47a38%2F83bd9df8-d524-4b2d-b583-739ab8bf4bb1%2FUntitled.png?table=block&amp;id=753310e9-def2-41f1-91eb-3369db52b37d" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-e5058ea39e3a4d4fb4f3751956a41e09">你必须从一个明确的目标开始。一个项目的时间过得很快，不知不觉中，你在一个项目上花了半年时间，却发现它对任何人都没有用。你有过这样的经历吗？这种情况发生的频率比您想象的要高，这就是为什么有些人对“工程师”失去信任，以及我们最终成为没有任何自主权的开发人员的原因。</div><div class="notion-text notion-block-6dcf62c81f21491081ebf3099be45c59">人们普遍担心我们需要“损失”多少时间来进行会话。考虑会话过程中损失的时间并不是正确的方法。相反，您应该考虑如果不进行该会话您将失去的好处。我在运行一次事件风暴会议时听到了这个故事，导致该项目的实施停止了几个月。这听起来可能很糟糕，但在会议期间，团队发现当前的假设完全无效。该项目的继续将导致彻底失败。即使该会议在短期内看起来很耗时，该公司也避免了几个月无用的开发。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-2f657a7a37da4c2a9499068d798ed221" data-id="2f657a7a37da4c2a9499068d798ed221"><span><div id="2f657a7a37da4c2a9499068d798ed221" class="notion-header-anchor"></div><a class="notion-hash-link" href="#2f657a7a37da4c2a9499068d798ed221" title="事件建模 "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">事件建模 </span></span></h3><div class="notion-text notion-block-b9891a95cec543f782d90fbd036fa700">2018 年，Adam Dymitruk 提出了事件建模技术。该想法在很大程度上基于事件风暴技术，但添加了一些新功能。它还特别强调了会议的用户体验部分。一般来说，这些技术非常兼容。即使您继续使用事件风暴，您也可能会从事件建模中找到一些可以使用的有价值的方法。您可以在 eventmodeling.org 上阅读有关该技术的更多信息。</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-5084187e07774c5391063ccf048170a7"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:720px;max-width:100%;flex-direction:column"><img style="object-fit:cover" src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Fb769cf6b-c125-41d2-8f10-c232f7a47a38%2F2e8db924-be17-4b05-ae09-832515e2f4b1%2FUntitled.png?table=block&amp;id=5084187e-0777-4c53-9106-3ccf048170a7" alt="notion image" loading="lazy" decoding="async"/></div></figure><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-a759e193d30f418a8ce6359b61fad7bb" data-id="a759e193d30f418a8ce6359b61fad7bb"><span><div id="a759e193d30f418a8ce6359b61fad7bb" class="notion-header-anchor"></div><a class="notion-hash-link" href="#a759e193d30f418a8ce6359b61fad7bb" title="界限上下文和事务边界（聚合） "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">界限上下文和事务边界（聚合） </span></span></h3><div class="notion-text notion-block-463de47ed61949f4bc76dbfb9dc99dcb">有界上下文是另一种战略 DDD 模式，可以帮助我们将大模型分割成更小的逻辑部分。</div><div class="notion-text notion-block-bb97c462f6f24218922483b2625e0ebc">这是实现适当的服务分离的关键。如果您需要接触系统的一半来实现和测试新功能，那么您的分离是错误的。</div><div class="notion-text notion-block-bea92d633f7e4932867f972ba3040914">除了<b>错误的分离</b>之外，还有<b>缺乏分离</b>。通常，缺乏分离的症状是神物体（知道太多或做太多的巨大物体）。在这种情况下，更改将主要影响一项服务。这种方法的成本是更大的系统故障风险和更高的变更复杂性。</div><div class="notion-text notion-block-b4d3441b51b542f185365d51812bf66d">换句话说，在这两种情况下，开发项目都会变得更加困难。</div><div class="notion-text notion-block-178cbb8474814a429e0908230e0c2f81">帮助发现界限上下文和聚合的一个很棒的工具当然是事件风暴。会议的结果是，您可以直观地了解应如何在服务和接触点之间划分服务和接触点。</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-481e0e67166c40b599927211486c70a3"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:100%;max-width:100%;flex-direction:column;height:100%"><img style="object-fit:cover" src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Fb769cf6b-c125-41d2-8f10-c232f7a47a38%2Fc34fa56f-1c41-40c0-b822-25f6a76ad179%2FUntitled.png?table=block&amp;id=481e0e67-166c-40b5-9992-7211486c70a3" alt="notion image" loading="lazy" decoding="async"/></div></figure><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-61274561d61442dabb60209adfb4a583" data-id="61274561d61442dabb60209adfb4a583"><span><div id="61274561d61442dabb60209adfb4a583" class="notion-header-anchor"></div><a class="notion-hash-link" href="#61274561d61442dabb60209adfb4a583" title="通用语言 "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">通用语言 </span></span></h3><div class="notion-text notion-block-b65d1bcfdbf34eb9bf3438942da9b6a0">通用语言是一种战略 DDD 模式，涵盖<b>在开发人员、运营、利益相关者和用户之间建立通用语言</b>。这是最被低估的战略 DDD 模式。因为谁关心语言，对吧？</div><div class="notion-text notion-block-4b0d1f91fe74418aa57f72cf830e0196">我花了一些时间才发现开发人员和非开发人员之间有多少沟通问题是因为使用不同的语言而造成的。这是多么痛苦啊。我鼓励你也关注这一点。由于沟通不畅，开发人员无法解决正确的问题，因为没有人了解对他们的期望。</div><div class="notion-text notion-block-3c69f95be8fb4acab30ae4579e86ac32">如果我告诉您事件风暴将帮助您开发通用语言，您会感到惊讶吗？与利益相关者一起举办会议迫使您与他们交谈。当你们无法互相理解时，就很难共同制定解决方案。这就是为什么不要错过研讨会上的利益相关者至关重要！</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-fdd20d6da31841d9a3baed8a5f37b3b1" data-id="fdd20d6da31841d9a3baed8a5f37b3b1"><span><div id="fdd20d6da31841d9a3baed8a5f37b3b1" class="notion-header-anchor"></div><a class="notion-hash-link" href="#fdd20d6da31841d9a3baed8a5f37b3b1" title="DDD 能解决所有问题吗？"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">DDD 能解决所有问题吗？</span></span></h3><div class="notion-text notion-block-56255ea935874ec8a35ce58d6af4a50d">即使 DDD 很棒，它也不能解决我们遇到的所有问题。管理你的期望很重要。即使我们在我的团队中高水平地使用这些技术，我们仍然怀疑创建的设计是否足够好。有时我们不知道如何解决问题。有时我们会从代码回到设计阶段。有时我们会做出错误的决定。所有这些都是非常好的情况。世界上没有一支球队不存在这些问题。最好假设它会发生并且不要感到惊讶。但我们知道，如果没有 DDD，这些问题将会更加严重。</div><div class="notion-text notion-block-35561493267c408a954b38dbbb6312ef">在使用 DDD 时，您应该特别注意避免：</div><ul class="notion-list notion-list-disc notion-block-9cafaeaa28864dcd9f92cf446801b448"><li>预先进行大设计</li></ul><ul class="notion-list notion-list-disc notion-block-a90afd5cfc9c43faa8f988e0eed05267"><li>实现“面向未来”的代码</li></ul><ul class="notion-list notion-list-disc notion-block-d1ea33d9387b4f7783a27fe9090feffd"><li>尝试创建完美的东西</li></ul><div class="notion-text notion-block-2ef89da1100541e5906a90a99e6419d7">相反，您应该：</div><ul class="notion-list notion-list-disc notion-block-600053b18946462cbd91bbb3705d4227"><li>专注于以一种快速的方式将 MVP 交付给用户时间很短（我的意思是1个月而不是6个月）</li></ul><ul class="notion-list notion-list-disc notion-block-d7b114b5cbc941fe8706beded234bc4a"><li>如果你需要“为了未来”实现一些东西，因为以后添加它会更困难，这是一个非常糟糕的迹象。您应该考虑如何方便以后添加它</li></ul><ul class="notion-list notion-list-disc notion-block-37138c79a0424ae28f31faa5368cb217"><li>即使你尽了最大努力，你的设计也不会从一开始就完美——随着时间的推移改进它会更好。</li></ul><div class="notion-text notion-block-7d1187000ef349bba20c0a5fc3c432ec">对于一些团队来说，可能需要做很多工作才能达到这个水平。但根据我的经验，我可以向你保证这是可能的。再次交付软件所带来的乐趣是值得的！</div><div class="notion-callout notion-gray_background_co notion-block-114e0749862f42938f56b55bb294027a"><div class="notion-page-icon-inline notion-page-icon-span"><span class="notion-page-icon" role="img" aria-label="💡">💡</span></div><div class="notion-callout-text">注意
如果您认为自己应该成为技术领导者来提出此类改进建议，那您就错了！早期，当我还不是领导者时，我已经向我工作的团队提出了许多改进建议。你需要与你的队友进行良好的争论。
我们总是在章节中解释这些技术“为什么”有效。当你使用这些论点时，它们应该足以说服他们。如果因为你的团队思想封闭而行不通，那么这就是考虑换工作的充分理由。</div></div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-4d9e19ccce2846fea60f48d3b3e0e200" data-id="4d9e19ccce2846fea60f48d3b3e0e200"><span><div id="4d9e19ccce2846fea60f48d3b3e0e200" class="notion-header-anchor"></div><a class="notion-hash-link" href="#4d9e19ccce2846fea60f48d3b3e0e200" title="软件复兴"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">软件复兴</span></span></h3><div class="notion-text notion-block-39f8e4b0418b4159acd7788374af86ff">很难在一章内深入介绍所介绍的技术。我的目标是激励你质疑现状。这不是我们的行业应该如何运作的。如果您对现状不满意，我希望我能激励您学习新技术。这是对抗软件黑暗时代的最佳方式。</div><div class="notion-text notion-block-18aaacd3b4cb440d802bafc47a42f042">战略 DDD 模式怎么样？本章实际上是对我们的章节系列的下一部分的介绍。在接下来的几个月中，我们将在一个完整的示例项目中深入介绍最重要的战略 DDD 模式。</div><div class="notion-text notion-block-874adfa7f435472db3356e95c4a1b86f">在软件黑暗时代，你是谁？</div><div class="notion-text notion-block-6300f77392624aa9bab074f6588280e6">只是一个盲目遵守别人强加的规则的普通乡巴佬？</div><div class="notion-text notion-block-562a911a9c724424bb166329db8fecef">宗教裁判所，谁会试图去憎恨和扼杀任何不寻常的做法？</div><div class="notion-text notion-block-8c9a23909a52462aa185a24b0255e769">炼金术士，想炼金子？</div><div class="notion-text notion-block-96ecc11c067c4537b6848f8ab3ab4b77">即使没有科学依据？或者也许您正在地窖里偷偷地阅读禁书？</div><div class="notion-text notion-block-b744fac9904d424780025b1b9ed694ee">也许您正在寻求与我们一起结束软件黑暗时代并开始软件复兴？</div></main></div>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[【读书笔记】《Go With The Domain》13. 在 CI/CD 管道中运行集成测试]]></title>
            <link>slw.ac.cn/article/go-with-the-domain-13.html</link>
            <guid>slw.ac.cn/article/go-with-the-domain-13.html</guid>
            <pubDate>Wed, 04 Oct 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[【关键词】整洁架构 DDD CQRS]]></description>
            <content:encoded><![CDATA[<div id="notion-article" class="mx-auto overflow-hidden "><main class="notion light-mode notion-page notion-full-width notion-block-b8cb51fb3a664cc8a4d173252241c284"><div class="notion-viewport"></div><div class="notion-collection-page-properties"></div><div class="notion-text notion-block-69852c101c1045b9abdf4f292eae30a7">这篇文章是测试架构的直接后续内容，我在其中为我们的示例项目引入了新类型的测试。 </div><div class="notion-text notion-block-c652c06161fe49a480114f95c4015b96">Wild Workouts 使用 Google Cloud Build 作为 CI/CD 平台。它以持续部署的方式进行配置，这意味着一旦管道通过，更改就会立即投入生产。如果你考虑一下我们目前的设置，你会发现它既勇敢又天真。我们没有在那里运行可以避免明显错误的测试（无论如何，不太明显的错误很少能被测试发现）。</div><div class="notion-text notion-block-979ddcdc7c1b4faa94e7597f5e3aa0d2">在本章中，我将展示如何使用 docker-compose 在 Google Cloud Build 上运行集成测试、组件测试和端到端测试。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-342218d8d7c3456cbb42560c24fee9eb" data-id="342218d8d7c3456cbb42560c24fee9eb"><span><div id="342218d8d7c3456cbb42560c24fee9eb" class="notion-header-anchor"></div><a class="notion-hash-link" href="#342218d8d7c3456cbb42560c24fee9eb" title="当前配置"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">当前配置</span></span></h3><div class="notion-text notion-block-f0e0d17561834a3da1de43b616c3e419">让我们看一下当前的<code class="notion-inline-code">cloudbuild.yaml</code> 文件。虽然非常简单，但由于我们在单个存储库中保留 3 个微服务，因此大多数步骤都会运行多次。我专注于后端部分，所以我现在将跳过与前端部署相关的所有配置。</div><div class="notion-text notion-block-4913c739294d48098e8faca00e265df8">注意 <code class="notion-inline-code">waitFor </code>键。它使一个步骤需要等待其他指定的步骤完成。有些作业可以通过这种方式并行运行。这是更易读的版本：</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-8fd63d7ba3134120915052291b6cee94"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:720px;max-width:100%;flex-direction:column"><img style="object-fit:cover" src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Fb769cf6b-c125-41d2-8f10-c232f7a47a38%2Feaf97a5e-5a77-4003-a300-52cd216919ec%2FUntitled.png?table=block&amp;id=8fd63d7b-a313-4120-9150-52291b6cee94" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-3a681ddae65047109c6b132057035fbd">我们对每项服务都有类似的工作流程：lint（静态分析）、构建 Docker 镜像并将其部署为一个或两个 Cloud Run 服务。</div><div class="notion-text notion-block-77365214ed5c46c298aea35c98ed78f7">由于我们的测试套件已准备就绪并可在本地运行，因此我们需要弄清楚如何将其插入管道中。 </div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-1b658fde66314abdbabeaa2919b49a15" data-id="1b658fde66314abdbabeaa2919b49a15"><span><div id="1b658fde66314abdbabeaa2919b49a15" class="notion-header-anchor"></div><a class="notion-hash-link" href="#1b658fde66314abdbabeaa2919b49a15" title="Docker Compose "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Docker Compose </span></span></h3><div class="notion-text notion-block-def094dacef94dc69fcb40ad7b3fd13d">我们已经有了一个 docker-compose 定义，我想保持这种方式。我们将使用它来：</div><ul class="notion-list notion-list-disc notion-block-72d777f6679a4e79b24fa231ce78bbb4"><li>本地运行应用程序</li></ul><ul class="notion-list notion-list-disc notion-block-8381b1f8aa8e4b10ac565a1828cee16d"><li>本地运行测试</li></ul><ul class="notion-list notion-list-disc notion-block-62429cd3004d4797adcbea316e69893e"><li>在CI 中运行测试</li></ul><div class="notion-text notion-block-47f3e2b564ff44af9f0a2876a13b987f">这三个目标有不同的需求。例如，当本地运行应用程序时，我们希望进行代码热重载。但这在 CI 中毫无意义。另一方面，我们无法在 CI 中公开本地主机上的端口，这是在本地环境中访问应用程序的最简单方法。</div><div class="notion-text notion-block-84a98e100c2f4181b6db00a6142016cd">幸运的是，docker-compose 足够灵活，可以支持所有这些用例。我们将使用一个基本的 <code class="notion-inline-code">docker-compose.yml</code>文件和一个附加的 <code class="notion-inline-code">docker-compose.ci.yml</code> 文件，其中仅覆盖 CI。您可以通过使用 <code class="notion-inline-code">-f</code> 标志传递两个文件来运行它（请注意，每个文件都有一个标志）。文件中的密钥将按提供的顺序合并。</div><div class="notion-callout notion-gray_background_co notion-block-5857d0053d4443b0b77019e777d4445d"><div class="notion-page-icon-inline notion-page-icon-span"><span class="notion-page-icon" role="img" aria-label="💡">💡</span></div><div class="notion-callout-text">注意
通常，docker-compose 会在当前目录或父目录中查找 docker-compose.yml 文件。使用<code class="notion-inline-code">-f</code>标志禁用此行为，因此仅解析指定的文件。</div></div><div class="notion-text notion-block-b3fe7f75dca34c028908cc1154fd454d">要在 Cloud Build 上运行它，我们可以使用 docker/compose 镜像。</div><div class="notion-text notion-block-ba0879ac719c40489f67b62dedf02f04">由于我们用正确的步骤名称填充了 <code class="notion-inline-code">waitFor</code>，因此我们可以确保存在正确的镜像。这是我们刚刚添加的内容：</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-6c69cd619c5346fca0d8c8d223e03d67"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:624px;max-width:100%;flex-direction:column"><img style="object-fit:cover" src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Fb769cf6b-c125-41d2-8f10-c232f7a47a38%2F33ed81ed-940e-4cb5-81f1-1ab0d2ed7899%2FUntitled.png?table=block&amp;id=6c69cd61-9c53-46fc-a0d8-c8d223e03d67" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-2b857b2e011348beba74f6ceddbec04f"> <code class="notion-inline-code">docker-compose.ci.yml </code>的第一个改造是使每个服务通过标签使用 docker 镜像，而不是从 <code class="notion-inline-code">docker/app/Dockerfile </code>构建镜像。这确保我们的测试检查我们要部署的相同镜像。</div><div class="notion-text notion-block-d36e0a0158fc43d3b8bf733d577bfa32">请注意图像键中的<code class="notion-inline-code"> ${PROJECT_ID} </code>变量，表示项目产品，因此我们不能将其硬编码到存储库中。 Cloud Build 在每个步骤中都提供了此变量，因此我们只需将其传递给 <code class="notion-inline-code">docker-compose up</code> 命令（请参阅上面的定义）。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-7dbc216e76ce4dffac27962dbce206a9" data-id="7dbc216e76ce4dffac27962dbce206a9"><span><div id="7dbc216e76ce4dffac27962dbce206a9" class="notion-header-anchor"></div><a class="notion-hash-link" href="#7dbc216e76ce4dffac27962dbce206a9" title="网络 "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">网络 </span></span></h3><div class="notion-text notion-block-ebfd1339158041c1a42acd14ff179e13">如今，许多 CI 系统都使用 Docker，通常在具有所选镜像的容器内运行每个步骤。在 CI 中使用 docker-compose 有点棘手，因为它通常意味着从 Docker 容器内运行 Docker 容器。</div><div class="notion-text notion-block-7268204b16e94d22af51a2ae488b5cfd">在 Google Cloud Build 上，所有容器都位于 <code class="notion-inline-code">cloudbuild </code>网络内。只需将此网络添加为 docker-compose.ci.yml 的默认网络就足以让 CI 步骤连接到 docker-compose 服务。这是我们的第二个改造：</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-d2979ba4c607455f979aa6a3aee048a3" data-id="d2979ba4c607455f979aa6a3aee048a3"><span><div id="d2979ba4c607455f979aa6a3aee048a3" class="notion-header-anchor"></div><a class="notion-hash-link" href="#d2979ba4c607455f979aa6a3aee048a3" title="环境变量 "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">环境变量 </span></span></h3><div class="notion-text notion-block-2e54974b40854020bcfb58943a62f8b9">使用环境变量作为配置乍一看似乎很简单，但考虑到我们需要处理多少场景，它很快就会变得复杂。让我们尝试列出所有这些：</div><ul class="notion-list notion-list-disc notion-block-0d06251c675c4f28aa78f8f27aed6db3"><li>在本地运行应用程序</li></ul><ul class="notion-list notion-list-disc notion-block-c2847eb02c274cbfb003c3037074d751"><li> 在本地运行组件测试</li></ul><ul class="notion-list notion-list-disc notion-block-15ee3ff0491446be9bd4b20284cf3e9a"><li>在 CI 中运行组件测试</li></ul><ul class="notion-list notion-list-disc notion-block-b3897a8a559c44f98b4461b2e352de60"><li>在本地运行端到端测试</li></ul><ul class="notion-list notion-list-disc notion-block-2111377d29ec435ba00b61870b4fc209"><li>在 CI 中运行端到端测试</li></ul><div class="notion-text notion-block-09d5727d403d47d5b79c95a190f3228d">我没有在生产环境中运行该应用程序，因为它不使用 docker-compose。</div><div class="notion-text notion-block-5675e3cd0ca142149af149714d03374c">为什么组件测试和端到端测试是不同的场景？前者按需启动服务，后者与 docker-compose 中已运行的服务进行通信。这意味着两种类型将使用不同的端点来访问服务。</div><div class="notion-callout notion-gray_background_co notion-block-221a9c1fc48d4bb8a3ff67f3909f2205"><div class="notion-page-icon-inline notion-page-icon-span"><span class="notion-page-icon" role="img" aria-label="💡">💡</span></div><div class="notion-callout-text">注意
有关组件和端到端测试的更多详细信息，请参阅测试架构。 TL;DR：我们重点关注组件测试，其中不包括外部服务。端到端测试只是为了确认合同没有在非常高的水平上被破坏，并且仅针对最关键的路径。这是服务解耦的关键。</div></div><div class="notion-text notion-block-ecabda30ccba4fd2ad6480521493d9cb">我们已经保留了一个包含大多数变量的基本 <code class="notion-inline-code">.env</code> 文件。它被传递给 docker-compose 定义中的每个服务。</div><div class="notion-text notion-block-7bec59c9d7a248d8a166e05bb9bf5efd">此外，当 docker-compose 在工作目录中找到该文件时，它会自动加载该文件。因此，我们也可以使用 yaml 定义中的变量。</div><div class="notion-text notion-block-51ad55f3c9c141b8b2afd0391a546a4c">我们还需要在运行测试时加载这些变量。在 bash 中这很容易做到：</div><div class="notion-text notion-block-eab37ad084954f9ea0ccd099f7de38b9">但是，<code class="notion-inline-code">.env</code> 文件中的变量没有导出前缀，因此它们不会进一步传递到 shell 中运行的应用程序。我们不能使用前缀，因为它与 docker-compose 期望的语法不兼容。</div><div class="notion-text notion-block-f8e005d8422b4c3bb19826f0b4c3aba5">此外，我们无法将单个文件用于所有场景。我们需要覆盖一些变量，就像我们对 docker-compose 定义所做的那样。我的想法是为每个场景保留一个附加文件。它将与基本 <code class="notion-inline-code">.env</code> 文件一起加载。</div><div class="notion-text notion-block-15524193accc4564b6ab88da69f37f87">让我们看看所有场景之间有什么区别。为了清楚起见，我只包含了 users-http，但这个想法将适用于所有服务。</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-3f5433e8427c457c950feadba6250221"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:768px;max-width:100%;flex-direction:column"><img style="object-fit:cover" src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Fb769cf6b-c125-41d2-8f10-c232f7a47a38%2F42ddd4dc-0dca-4312-8638-c1f39017d71f%2FUntitled.png?table=block&amp;id=3f5433e8-427c-457c-950f-eadba6250221" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-1342a65f5f69434a91622d85b855b0c5">docker-compose 运行的服务使用端口 3000+，组件测试在端口 5000+ 上启动服务。这样，两个实例就可以同时运行。</div><div class="notion-text notion-block-0e2759c59c6645578d30b15bfd069120">我创建了一个 bash 脚本来读取变量并运行测试。<b>请不要尝试直接在 Makefile 中定义如此复杂的场景</b>。Make 在管理环境变量方面很糟糕。</div><div class="notion-text notion-block-03e8fa089885405fb5784f4631f325a5">创建专用脚本的另一个原因是我们将 3 个服务保留在一个存储库中，并将端到端测试保留在一个单独的目录中。如果我需要多次运行相同的命令，我更喜欢调用带有两个变量的脚本，而不是使用长长的标志和参数。第三个原因是它们可以用 shellcheck 进行检查。</div><div class="notion-text notion-block-ad4ef28450334455b3b06d1fd1f8c634">该脚本在给定目录中运行<code class="notion-inline-code">go test</code>，并使用从<code class="notion-inline-code"> .env</code> 和指定文件加载的环境变量。<code class="notion-inline-code">env / xargs</code> 技巧将所有变量传递给以下命令。请注意我们如何使用 <code class="notion-inline-code">grep </code>从文件中删除注释。</div><div class="notion-callout notion-gray_background_co notion-block-ea36c7f6bb8d4022a1fb9b553ef0cf9c"><div class="notion-page-icon-inline notion-page-icon-span"><span class="notion-page-icon" role="img" aria-label="💡">💡</span></div><div class="notion-callout-text">测试缓存
go test 会缓存成功的结果，只要不修改相关文件即可。在使用 Docker 的测试中，您可能会更改基础设施级别的某些内容，例如 <b>docker-compose 定义或某些环境变量。 go test 不会检测到这一点</b>，并且您可能会将缓存的测试误认为是成功的测试。
对此很容易感到困惑，而且由于我们的测试无论如何都很快，因此我们可以<b>禁用缓存</b>。 <code class="notion-inline-code">-count=1</code> 标志是一种惯用的（尽管不明显）方法。</div></div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-f927284ebff6456e9267fde6bb47d844" data-id="f927284ebff6456e9267fde6bb47d844"><span><div id="f927284ebff6456e9267fde6bb47d844" class="notion-header-anchor"></div><a class="notion-hash-link" href="#f927284ebff6456e9267fde6bb47d844" title="运行测试 "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">运行测试 </span></span></h3><div class="notion-text notion-block-13ed916648bd4231a35cac08d02228bf">在所有服务的测试通过后，开始运行端到端测试。它应该类似于您通常运行它们的方式。请记住，端到端测试应该起到双重检查的作用，并且每个服务自己的测试应该具有最大的覆盖范围。</div><div class="notion-text notion-block-45c0c70959754183a1898767be9c44ad">由于我们的端到端测试范围很小，因此我们可以<b>在部署服务之前运行它们</b>。如果它们运行很长时间，这可能会阻止我们的部署。在这种情况下，更好的想法是依赖每个服务的组件测试同时并行运行端到端套件。</div><div class="notion-text notion-block-774910ced39e4bae9eaba2b64d52bc16">我们添加的最后一件事是在所有测试通过后运行 docker-compose。这只是一次清理。</div><div class="notion-text notion-block-a5a8022e7c8b454e970f26c3a3114b6a">我们管道的第二部分现在看起来像这样：</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-71366c3dd390451eab08b20a35eb1dbd"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:720px;max-width:100%;flex-direction:column"><img style="object-fit:cover" src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Fb769cf6b-c125-41d2-8f10-c232f7a47a38%2Fa177fed2-a838-45f0-a4ea-6282d2c38b51%2FUntitled.png?table=block&amp;id=71366c3d-d390-451e-ab08-b20a35eb1dbd" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-2cbe41b22cd044619bed18986924040a">以下是本地运行测试的样子（我在上一章中介绍了此Make Target）。它与CI完全相同，只有不同的.env文件</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-86d02c2af1ab4136bc7601e2d96b7d4c" data-id="86d02c2af1ab4136bc7601e2d96b7d4c"><span><div id="86d02c2af1ab4136bc7601e2d96b7d4c" class="notion-header-anchor"></div><a class="notion-hash-link" href="#86d02c2af1ab4136bc7601e2d96b7d4c" title="分离测试"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">分离测试</span></span></h3><div class="notion-text notion-block-51c6fbe349654a639230d469b22c436f">查看上一章中的表格，根据是否使用 Docker，我们可以将测试分为两组。</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-996e985cfb134b0b9982e33f2f1381d8"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:672px;max-width:100%;flex-direction:column"><img style="object-fit:cover" src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Fb769cf6b-c125-41d2-8f10-c232f7a47a38%2Fbded0c60-340c-4659-87ef-e4b0e4522767%2FUntitled.png?table=block&amp;id=996e985c-fb13-4b0b-9982-e33f2f1381d8" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-a2f8faec32aa4cf596fed21854c7f048">单元测试是唯一不使用 Docker 数据库的类别，而集成、组件和端到端测试则使用 Docker 数据库。</div><div class="notion-text notion-block-0fe6cf0cf64c4dadb9f3b7509b34a05b">尽管我们使所有测试都变得快速且稳定，但设置 Docker 基础设施会增加一些开销。单独运行所有单元测试很有帮助，可以首先防止错误。</div><div class="notion-text notion-block-4abd1df40cef43d187d67d6224de001e">我们可以使用构建标签来分隔不使用 Docker 的测试。您可以在文件的第一行定义构建标签。</div><div class="notion-text notion-block-c0b82e90d93f420a87a525b6a2ea2332">我们现在可以与所有测试分开运行单元测试。例如，下面的命令将仅运行需要 Docker 服务的测试：</div><div class="notion-text notion-block-95de904bb7864218a3c97cc6b3cf4250">最后，我决定不引入这种分离。我们使测试足够稳定和快速，因此一次运行所有测试并不是问题。然而，我们的项目非常小，测试套件只涵盖了关键路径。<b>当它增长时，在组件测试中引入构建标签可能是个好主意</b>。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-5f4cee56de0f450d8f7e9349f83caeb4" data-id="5f4cee56de0f450d8f7e9349f83caeb4"><span><div id="5f4cee56de0f450d8f7e9349f83caeb4" class="notion-header-anchor"></div><a class="notion-hash-link" href="#5f4cee56de0f450d8f7e9349f83caeb4" title="题外话：一个关于 CI 调试的小故事 "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">题外话：一个关于 CI 调试的小故事 </span></span></h3><div class="notion-text notion-block-bbf587d6f24946b3aba7b1e7bd714130">当我介绍本章的更改时，Cloud Build 上的初始测试运行一直失败。根据日志，测试无法从 docker-compose 访问服务。</div><div class="notion-text notion-block-06d26a03271847729c8055055426aaba">因此，我开始调试并添加了一个简单的 bash 脚本，该脚本将通过 telnet 连接到服务。</div><div class="notion-text notion-block-cccd700ef6254d7fbeb51409e417661c">令我惊讶的是，连接到 mysql:3306 工作正常，但 firestore:8787 没有，所有 Wild Workouts 服务也是如此。我认为这是因为 docker-compose 需要很长时间才能启动，但是多次重试都没有帮助。最后，我决定尝试一些疯狂的事情，我从 docker-compose 中的一个容器设置了一个反向 SSH 隧道。这允许我在构建仍在运行时在其中一个容器内进行 SSH。然后我尝试使用 telnet 和curl，它们对所有服务都能正常工作。</div><div class="notion-text notion-block-af6b9e92759543c38dd4ee22f1551490">最后，我在我使用的 bash 脚本中发现了一个错误</div><div class="notion-text notion-block-e440c045fcac4c16971143fd94f66edf">变量定义中的拼写错误导致 telnet 命令运行变成了：<code class="notion-inline-code">telnet $host $host</code>。那么为什么它对 MySQL 有效呢？事实证明，telnet 可以识别<code class="notion-inline-code">/etc/services</code> 中定义的端口。因此 <code class="notion-inline-code">telnet mysql mysql</code> 被转换为 <code class="notion-inline-code">telnet mysql 3306</code> 并且工作正常，但对于任何其他服务都失败了。</div><div class="notion-text notion-block-a9b457e93ab24eae9b1298ceb519c6ad">为什么测试失败了？好吧，事实证明这是一个完全不同的原因。</div><div class="notion-text notion-block-98dc7e18a8d74562b21eb6477df16161">最初，我们是这样连接MySQL的：</div><div class="notion-text notion-block-aa7dda07252c4684a2973ddc43b219a0">我查看了环境变量，所有这些都正确填写。添加一些 <code class="notion-inline-code">fmt.Println() </code>调试后，我发现配置的<b> Addr 部分被 MySQL 客户端完全忽略</b>，因为我们没有指定 Net 字段。为什么它适用于本地测试？因为MySQL暴露在localhost上，这是默认地址。</div><div class="notion-text notion-block-2cf5d6af1dc941c0961c8984a48cba0d">另一个测试无法连接到 Wild Workouts 服务之一，结果是因为我在 .env 文件中使用了不正确的端口。</div><div class="notion-text notion-block-231d7fa244d44721a2853a7c4fab9c8f">我为什么要分享这个？我认为这是一个很好的例子，展示了 CI 系统的工作方式。当多项事情都可能失败时，很容易得出错误的结论并在错误的方向上深入挖掘。</div><div class="notion-text notion-block-c2bbdee7aa8f465da0308cfa463b7342">如有疑问，我喜欢使用基本工具来调查 Linux 问题，例如 strace、curl 或 telnet。这也是我使用反向 SSH 隧道的原因，我很高兴我这么做了，因为这似乎是调试 CI 内部问题的好方法。我觉得有时间我会再次使用它。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-e41bb12a88d44468884120a2755d17e2" data-id="e41bb12a88d44468884120a2755d17e2"><span><div id="e41bb12a88d44468884120a2755d17e2" class="notion-header-anchor"></div><a class="notion-hash-link" href="#e41bb12a88d44468884120a2755d17e2" title="总结 "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">总结 </span></span></h3><div class="notion-text notion-block-b9442833494745058bb2bf20f754abab">我们设法保留一个 docker-compose 定义，用于在本地和管道中运行测试。从 git Push 到生产的整个 Cloud Build 运行需要 4 分钟。我们使用了一些巧妙的技巧，但这只是处理 CI 时的常规内容。有时你无法避免添加一些 bash 魔法来使事情正常进行。与领域代码相比，CI 设置中的黑客攻击不会对您造成太大伤害，只要其中只有少数。只要确保它很容易理解正在发生的事情，这样你就不是唯一拥有所有知识的人。</div></main></div>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[【读书笔记】《Go With The Domain》12. 存储库设计安全]]></title>
            <link>slw.ac.cn/article/go-with-the-domain-12.html</link>
            <guid>slw.ac.cn/article/go-with-the-domain-12.html</guid>
            <pubDate>Wed, 04 Oct 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[【关键词】整洁架构 DDD CQRS]]></description>
            <content:encoded><![CDATA[<div id="notion-article" class="mx-auto overflow-hidden "><main class="notion light-mode notion-page notion-full-width notion-block-e2924a2a4e3548a8934415ff3076c79d"><div class="notion-viewport"></div><div class="notion-collection-page-properties"></div><div class="notion-text notion-block-e3931bda17d24638b580e7861c45cd75">通过测试和代码审查，您可以使您的项目没有错误。是吗？嗯……实际上，可能不是。那太简单了。这些技术降低了出现错误的可能性，但不能完全消除它们。但这是否意味着我们需要忍受错误的风险直到生命的尽头？</div><div class="notion-text notion-block-a9daf567a62f454d87a905fbd0b8b0cb">一年多前，我在Harbor项目中发现了一个非常有趣的 PR。这是对允许普通用户创建管理员用户这个问题的修复。这显然是一个严重的安全问题。当然，自动化测试之前并没有发现这个bug。错误修复如下所示：</div><div class="notion-text notion-block-eea65160072b4044b87ffb6b045d1ca9">一条 if 语句修复了该错误。添加新的测试还应该确保将来不会出现回归。够了吗？它是否可以保护应用程序在未来免受类似错误的影响？我很确定没有。</div><div class="notion-text notion-block-8488ec793412439ca753e83777902011">在更复杂的系统中，如果有一个大团队在处理这些系统，问题就会变得更大。如果有人是该项目的新手并且忘记添加此 if 语句怎么办？您可能会惊讶于您编写的代码的寿命有多长。我们不应该相信人们会按照预期的方式使用我们创建的代码——他们不会。</div><div class="notion-text notion-block-8b2d42793e3a410f9965072ee3cbf9e4">在某些情况下，能够保护我们免受此类问题影响的解决方案就是<b>良好的设计</b>。好的设计不应允许以无效的方式使用我们的代码。好的设计应该保证你可以毫无恐惧地接触现有的代码。新加入该项目的人会觉得引入变更更安全。</div><div class="notion-text notion-block-4597bbf57b5a4d5eb2a4c8321af4de7f">在本章中，我将展示如何确保只有允许的人才能查看和编辑训练。</div><div class="notion-text notion-block-68841b58efb544758df2307b393cd0e7">在我们的例子中，只有训练所有者（学员）和教练才能看到训练。我将以一种不允许以非预期方式使用我们的代码的方式来实现它。通过设计。我们当前的应用程序假设访问数据的唯一方式是通过存储库。因此，我将在存储库添加授权级别。由此，我们确信未经授权的用户不可能访问这些数据。</div><div class="notion-callout notion-gray_background_co notion-block-710848b944f34738811e8d9829e0235b"><div class="notion-page-icon-inline notion-page-icon-span"><span class="notion-page-icon" role="img" aria-label="💡">💡</span></div><div class="notion-callout-text">什么是存储库 tl;dr 
如果您没有机会阅读我们前面的章节，那么存储库是一种帮助我们从应用程序逻辑中抽象数据库实现的模式。如果您想更多地了解它的优点并了解如何将其应用到您的项目中，请阅读存储库模式。</div></div><div class="notion-text notion-block-9e1c814bb0a34b5d9e2d781229542f38">但是等等，存储库是管理授权的正确位置吗？嗯，我可以想象有些人可能会对这种方法持怀疑态度。当然，我们可以开始一些关于存储库中可以包含哪些内容和不应该包含哪些内容的哲学讨论。另外，谁可以看到训练的实际逻辑将被<b>放置在领域层</b>。我没有看到任何重大缺点，而且优点也很明显。在我看来，实用主义应该在这里获胜。</div><div class="notion-callout notion-gray_background_co notion-block-fde439d524b34c5099b6b34682072378"><div class="notion-page-icon-inline notion-page-icon-span"><span class="notion-page-icon" role="img" aria-label="💡">💡</span></div><div class="notion-callout-text">提示
本书的另一个有趣之处是我们重点关注面向业务的应用程序。但即使 Harbor 项目是一个纯粹的系统应用程序，大多数提出的模式也可以应用。在向我们团队介绍 Clean Architecturea 后，我们的队友在他的游戏中使用了这种方法来抽象渲染引擎。</div></div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-3927aee85f3b4d3fbaa37b16d5c48ea4" data-id="3927aee85f3b4d3fbaa37b16d5c48ea4"><span><div id="3927aee85f3b4d3fbaa37b16d5c48ea4" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3927aee85f3b4d3fbaa37b16d5c48ea4" title="请告诉我代码！"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">请告诉我代码！</span></span></h3><div class="notion-text notion-block-8ae99dc7a78446edbf81f66fc35309e0">为了实现我们的稳健设计，我们需要实现三件事：</div><ol start="1" class="notion-list notion-list-numbered notion-block-0074edbd661b4b1cb579287c8c507082"><li>谁能看到训练的逻辑（领域层）</li></ol><ol start="2" class="notion-list notion-list-numbered notion-block-1fb0f77f2e5d4907bcaece0bd9467dff"><li>用于获取训练的函数（存储库中的 <code class="notion-inline-code">GetTraining</code>）</li></ol><ol start="3" class="notion-list notion-list-numbered notion-block-a8b5341a2a53491eaded139efc3e452b"><li>用于更新训练的函数（存储库中的<code class="notion-inline-code">UpdateTraining</code>）</li></ol><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-6f2d528e6a574265aa531d59eb7ff968" data-id="6f2d528e6a574265aa531d59eb7ff968"><span><div id="6f2d528e6a574265aa531d59eb7ff968" class="notion-header-anchor"></div><a class="notion-hash-link" href="#6f2d528e6a574265aa531d59eb7ff968" title="领域层 "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">领域层 </span></span></h3><div class="notion-text notion-block-962b1566632f459c90d3da1d2cf2bc55">第一部分是负责决定某人是否可以看到训练的逻辑。因为它是领域逻辑的一部分（你可以和你的业务或产品团队讨论谁可以看到培训），所以它应该进入领域层。它是通过 <code class="notion-inline-code">CanUserSeeTraining </code>函数实现的。将其保留在存储库级别也是可以接受的，但很难重用。我不认为这种方法有任何优势——特别是如果将其放入领域不需要任何成本的话。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-f7056a9830a34618ab996ced308a1ae6" data-id="f7056a9830a34618ab996ced308a1ae6"><span><div id="f7056a9830a34618ab996ced308a1ae6" class="notion-header-anchor"></div><a class="notion-hash-link" href="#f7056a9830a34618ab996ced308a1ae6" title="Repository "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Repository </span></span></h3><div class="notion-text notion-block-913d40c8d5874cb99c7efb8a0ae947aa">现在当我们有了 <code class="notion-inline-code">CanUserSeeTraining </code>函数后，我们需要使用这个函数。就这么简单。</div><div class="notion-text notion-block-f97ef274e7c6430c8fc46f99ec494960">是不是太简单了？我们的目标是创建简单而不复杂的设计和代码。这是一个很好的迹象，表明它非常简单。我们正在以同样的方式更改 <code class="notion-inline-code">UpdateTraining</code>。</div><div class="notion-text notion-block-db9eb1fb3bcc4b8ba9e18f35453fc02c">就这样！有什么办法可以让有人以错误的方式使用它吗？只要用户有效 – 就不会。这种方法类似于领域驱动设计精简版中介绍的方法。这一切都是为了创建我们不能以错误的方式使用的代码。这是 <code class="notion-inline-code">UpdateTraining </code>现在的用法：</div><div class="notion-text notion-block-60f41016a884491291577aac13b141f8">当然，如果可以重新安排 Training，还需要设置其他一些规则，但同样也是由 Training 领域类型处理的</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-43d0227681f240efb2132f5565b0b742" data-id="43d0227681f240efb2132f5565b0b742"><span><div id="43d0227681f240efb2132f5565b0b742" class="notion-header-anchor"></div><a class="notion-hash-link" href="#43d0227681f240efb2132f5565b0b742" title="处理集合"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">处理集合</span></span></h3><div class="notion-text notion-block-f837c3ac37424741a2b183f32e8041f3">即使这种方法非常适合单个训练的操作，您也需要确保对训练集合的访问得到适当的保护。这里没有魔法：</div><div class="notion-text notion-block-aa51ff0973704b1a8e5eabaf8e82f5d9">在应用程序层上执行 <code class="notion-inline-code">CanUserSeeTraining </code>函数将非常缓慢且代价大。最好创建一些重复的逻辑。</div><div class="notion-text notion-block-c6ad00ea2cb84cf4a146e5c82844262c">如果此逻辑在您的应用程序中更复杂，您可以尝试在领域层中将其抽象为可转换为数据库驱动程序中的查询参数的格式。我做过一次，效果非常好。</div><div class="notion-text notion-block-4add00d7a3b748f8a42f45dfcd35809c">但在Wild Workouts中，它会增加不必要的复杂性 - 让我们保持简单。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-d8288a3a9d084b5f968323ce83a939f3" data-id="d8288a3a9d084b5f968323ce83a939f3"><span><div id="d8288a3a9d084b5f968323ce83a939f3" class="notion-header-anchor"></div><a class="notion-hash-link" href="#d8288a3a9d084b5f968323ce83a939f3" title="处理内部更新"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">处理内部更新</span></span></h3><div class="notion-text notion-block-356f18be395d4b78a7a27d3e0df61cfe">我们通常希望拥有允许开发人员或公司运营部门进行一些“后门”更改的端点。在这种情况下，你能做的最糟糕的事情就是创建任何类型的“假用户”和黑客攻击。</div><div class="notion-text notion-block-b3894f5be6bc4dc9b5f48e4eb712e2c3">根据我的经验，它需要通过代码中的大量 if 语句来处理。它还会混淆审核日志（如果有的话）。与其创建“假用户”，不如创建一个特殊角色并显式定义该角色的权限。</div><div class="notion-text notion-block-3368bf6532db453594ddd6ccf7f0befe">如果您需要不用任何用户的存储库方法（对于 Pub/Sub 消息处理程序或迁移），最好创建单独的存储库方法。在这种情况下，命名至关重要——我们需要确保使用该方法的人知道安全隐患。</div><div class="notion-text notion-block-577b4476d7ce4604b240d5492048d701">根据我的经验，如果不同参与者的更新变得非常不同，甚至值得为每个参与者引入单独的 CQRS 命令。在我们的例子中，它可能是 <code class="notion-inline-code">UpdateTrainingByOperations</code>。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-f586c85f965d444e970f7ce294f7c764" data-id="f586c85f965d444e970f7ce294f7c764"><span><div id="f586c85f965d444e970f7ce294f7c764" class="notion-header-anchor"></div><a class="notion-hash-link" href="#f586c85f965d444e970f7ce294f7c764" title="通过 context.Context 传递身份验证 "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">通过 <code class="notion-inline-code">context.Context</code> 传递身份验证 </span></span></h3><div class="notion-text notion-block-14430673eadd40e98b783d19c2565cee">据我所知，有些人通过 <code class="notion-inline-code">context.Context</code> 传递身份验证详细信息。</div><div class="notion-text notion-block-cd9f3a0ff00b4346a10d5ff143d8ccb9">我强烈建议<b>不要通过 </b><code class="notion-inline-code"><b>context.Context</b></code><b> 传递</b>应用程序正常工作所需的任何内容。原因很简单——当通过 <code class="notion-inline-code">context.Context</code> 传递值时，我们失去了 Go 最重要的优势之一——静态类型。它还隐藏了函数的输入到底是什么。</div><div class="notion-text notion-block-227ca39eedae4071b993719d85b345fd">如果您出于某种原因需要通过上下文传递值，这可能是您服务中某处设计不良的症状。也许该函数做了太多的事情，并且很难将所有参数传递到那里？也许是时候分解它了？</div><div class="notion-text notion-block-8ae73a2b667f4017b52623c759716e80">这就是今天的全部内容！如您所见，所提出的方法易于快速实施。希望对您的项目有所帮助，让您对未来的发展更有信心。</div></main></div>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[【读书笔记】《Go With The Domain》11. 测试架构]]></title>
            <link>slw.ac.cn/article/go-with-the-domain-11.html</link>
            <guid>slw.ac.cn/article/go-with-the-domain-11.html</guid>
            <pubDate>Wed, 04 Oct 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[【关键词】整洁架构 DDD CQRS]]></description>
            <content:encoded><![CDATA[<div id="notion-article" class="mx-auto overflow-hidden "><main class="notion light-mode notion-page notion-full-width notion-block-82d418778cb4408cae6473853ff1cc87"><div class="notion-viewport"></div><div class="notion-collection-page-properties"></div><div class="notion-text notion-block-ec5bae466c204485aa9b1eecdd983f79">您是否知道当您从头开始开发新应用程序，并可以通过适当的测试覆盖所有流程时的罕见感觉？</div><div class="notion-text notion-block-8c06353396a945dfb1c9f1fe7c01be7e">我说“罕见”是因为大多数时候，您将使用历史悠久、贡献者众多且测试方法不那么明显的软件。即使代码使用了好的模式，测试套件也并不总是遵循。</div><div class="notion-text notion-block-bdc5a5ee00fe44dab7bf5ee25ee93daa">有些项目没有设置现代化的开发环境，因此只有易于测试的东西才进行单元测试。例如，他们单独测试单个功能，因为很难测试公共 API。团队需要手动验证所有更改，可能是在某种临时环境中。您知道当有人引入更改但不知道他们需要手动测试时会发生什么。</div><div class="notion-text notion-block-d13d70966174433fab415a5e74d97e77">其他项目从一开始就没有测试。它可以通过走捷径来加快开发速度，例如将依赖关系保持在全局状态。当团队意识到缺乏测试会导致错误并减慢它们的速度时，他们决定添加它们。但现在，想要合理地做到这一点是不可能的。因此，团队编写了一个具有适当基础设施的端到端测试套件。</div><div class="notion-text notion-block-dcce4050bb394a92a2d7f2ad43adb764">端到端测试可能会给您一些信心，但您不想维护这样的测试套件。它很难调试，即使是最简单的更改也需要很长时间来测试，并且发布应用程序需要几个小时。在这种情况下引入新的测试也不是小事，因此开发人员尽可能避免它。</div><div class="notion-text notion-block-34eb7c72427146f485ef9064b8ac58ff">我想介绍一些迄今为止对我们有用的想法，应该可以帮助您避免上述情况。</div><div class="notion-text notion-block-49d824c3422c4d86b5ad3486478c2e10">本章不是关于哪个测试库最好或者可以使用哪些技巧（尽管我将展示一些技巧）。它更接近我所说的“测试架构”。这不仅涉及“如何”，还涉及“在哪里”、“什么”和“为什么”。</div><div class="notion-text notion-block-947b9072c15042b585d5701dbef0b80f">关于不同类型的测试有很多讨论，例如“测试金字塔”（Robert 在高质量数据库集成测试中提到过）。这是一个值得记住的有用模型。然而，它也是抽象的，你无法轻易测量它。我想采取更实用的方法，展示如何在 Go 项目中引入几种测试。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-73361adb61074524891da2e4838b49aa" data-id="73361adb61074524891da2e4838b49aa"><span><div id="73361adb61074524891da2e4838b49aa" class="notion-header-anchor"></div><a class="notion-hash-link" href="#73361adb61074524891da2e4838b49aa" title="为什么要为测试烦恼呢？"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">为什么要为测试烦恼呢？</span></span></h3><div class="notion-text notion-block-2bea5f4d68d64055906f039fb24b0bda">但是测试代码难道不像应用程序的其余部分那么重要吗？难道我们不能接受保持测试良好状态是困难的事实并继续前进吗？不是可以加快开发速度吗？</div><div class="notion-text notion-block-cbd098805a814f498c6182ee73e407f1">如果您一直关注本系列，您就会知道我们的所有章节都基于 Wild Workouts 应用程序。</div><div class="notion-text notion-block-b69b386199b24ee98ab67de6c2282c62">当我开始编写本章时，本地运行测试对我来说甚至无法正常工作，而且这是一个相对较新的项目。</div><div class="notion-text notion-block-2812eba91db1473b95e43fef33fac428">发生这种情况的原因之一是：我们没有在 CI 管道中运行测试。</div><div class="notion-text notion-block-6f36a60354104dcaaef48b1862955366">这令人震惊，但似乎即使是使用最流行、最前沿技术的无服务器、云原生应用程序也可能是混乱中伪装出来的。</div><div class="notion-text notion-block-577dc0f785a146539163e564c314e1ed">我们知道现在应该向管道添加测试。众所周知，这让您有信心将更改安全地部署到生产中。然而，这也是有代价的。</div><div class="notion-text notion-block-1b1771d727b642fbb0ff85e194b0ce82">运行测试可能会占用管道持续时间的很大一部分。如果您没有以与应用程序代码相同的质量来设计和实现它们，您可能会意识到得太晚了，管道需要一小时才能通过，并且会随机失败。即使您的应用程序代码设计良好，测试也可能成为交付更改的瓶颈。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-fc63a56eb58942df9855be5f1f92ed4a" data-id="fc63a56eb58942df9855be5f1f92ed4a"><span><div id="fc63a56eb58942df9855be5f1f92ed4a" class="notion-header-anchor"></div><a class="notion-hash-link" href="#fc63a56eb58942df9855be5f1f92ed4a" title="分层"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">分层</span></span></h3><div class="notion-text notion-block-e13854866e4743ce93a74dd70581291a">我们对该项目进行了几次重构。我们引入了像 Repository这样的模式。通过关注点分离，我们可以更轻松地推理项目的特定部分。</div><div class="notion-text notion-block-900c744a7a074701a6aeda8107bae56a">让我们回顾一下在前面的章节中介绍过的分层的概念。如果您之前没有机会阅读这些内容，我建议您在继续之前阅读这些内容 - 这将帮助您更好地理解本章。看一下图表可以帮助我们理解项目的结构。以下是使用 Wild Workouts 中使用的方法构建的通用服务</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-c1a633c6ece1477e8858b9bc6ff52b2b"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:1008px;max-width:100%;flex-direction:column"><img style="object-fit:cover" src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Fb769cf6b-c125-41d2-8f10-c232f7a47a38%2F7454ba5c-a633-4966-ba99-1dce7ddd75e1%2FUntitled.png?table=block&amp;id=c1a633c6-ece1-477e-8858-b9bc6ff52b2b" alt="整体框架" loading="lazy" decoding="async"/><figcaption class="notion-asset-caption">整体框架</figcaption></div></figure><div class="notion-text notion-block-802638f534be40b5ac85bcf9d30ef908">所有外部输入都从左侧开始。应用程序的唯一入口点是通过端口层（HTTP 处理程序、Pub/Sub 消息处理程序）。端口在应用层执行相关处理程序。其中有些会调用Domain代码，有些会使用Adapter，这是退出服务的唯一途径。适配器层是数据库查询和 gRPC 客户端所在的位置。</div><div class="notion-text notion-block-f74c31344d834f90bcaaee4586572e27">下图显示了 Wild Workouts 中部分trainer服务的层次和流程。</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-40ff829553574a7ba60a180f254fb43b"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:1008px;max-width:100%;flex-direction:column"><img style="object-fit:cover" src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Fb769cf6b-c125-41d2-8f10-c232f7a47a38%2F3fa2599a-5c91-4314-abc1-7a8d5cffec33%2FUntitled.png?table=block&amp;id=40ff8295-5357-4a7b-a60a-180f254fb43b" alt="trainer服务" loading="lazy" decoding="async"/><figcaption class="notion-asset-caption">trainer服务</figcaption></div></figure><div class="notion-text notion-block-94564e42a16a425886e754f09de618a8">现在让我们看看需要哪些类型的测试来涵盖所有内容。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-f5d365f961df42c3856671f0e389aa40" data-id="f5d365f961df42c3856671f0e389aa40"><span><div id="f5d365f961df42c3856671f0e389aa40" class="notion-header-anchor"></div><a class="notion-hash-link" href="#f5d365f961df42c3856671f0e389aa40" title="单元测试 "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">单元测试 </span></span></h3><div class="notion-text notion-block-3eebc2c7376a43eaa7e0643a76f03375">我们从内层和每个人都熟悉的东西开始：单元测试。</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-66de5b95d6974cf4a785d35f8ddc880a"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:624px;max-width:100%;flex-direction:column"><img style="object-fit:cover" src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Fb769cf6b-c125-41d2-8f10-c232f7a47a38%2Fb138346b-ca52-4484-8e84-99e79ceb5f05%2FUntitled.png?table=block&amp;id=66de5b95-d697-4cf4-a785-d35f8ddc880a" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-b72b0d3efb1246a6aae46087e9390212">领域层是服务中最复杂的逻辑所在的地方。然而，这里的测试应该是<b>一些最简单的编写和运行速度超快的测试</b>。领域中没有外部依赖项，因此您不需要任何特殊的基础设施或模拟（除了非常复杂的场景，但我们暂时保留它）。</div><div class="notion-text notion-block-95bc8b193dba449fbc6c6ede0967283a">根据经验，您应该瞄准领域层的高测试覆盖率。确保仅测试导出的代码（黑盒测试）。将 <code class="notion-inline-code">_test</code> 后缀添加到包名称中是强制执行此操作的一个很好的做法。</div><div class="notion-text notion-block-4f7768defd4e4da48016c87ea42d620c">领域代码是纯逻辑且易于测试，因此它是检查所有极端情况的最佳位置。表驱动测试对此尤其有用。</div><div class="notion-text notion-block-284b9071fa6144d7be159f8a72a4865a">我们离开领域，进入应用层。引入 CQRS 后，我们将其进一步分为命令和查询。</div><div class="notion-text notion-block-8e91951e8017499db76be77c3f3d8014">根据您的项目，可能不需要测试任何内容或需要涵盖一些复杂的场景。大多数时候，尤其是在查询中，此代码只是将其他层粘合在一起。测试这个不会增加任何价值。但如果命令中有任何复杂的编排，那么这就是单元测试的另一个好例子。</div><div class="notion-callout notion-gray_background_co notion-block-6e65b0c8348b478a9f7d5ad4f157ebb4"><div class="notion-page-icon-inline notion-page-icon-span"><span class="notion-page-icon" role="img" aria-label="💡">💡</span></div><div class="notion-callout-text">注意应用程序层中的复杂逻辑。如果您在这里开始测试业务场景，那么值得考虑引入领域层。
另一方面，它是编排的完美场所——以特定顺序调用适配器和服务并传递返回值。如果程序像之前那样分层，那么每次更改领域代码时应用程序测试都不应该受影响。</div></div><div class="notion-text notion-block-871bcbcfb13749f28716807c178d7d21">与领域代码相反，应用程序的命令和查询中有许多外部依赖项。如果您遵循 Clean Architecture，那么很容易对他们进行mock。在大多数情况下，具有单个方法的结构可以实现完美的mock。</div><div class="notion-callout notion-gray_background_co notion-block-c7ce43c0b8ce4302828234e4619242c8"><div class="notion-page-icon-inline notion-page-icon-span"><span class="notion-page-icon" role="img" aria-label="💡">💡</span></div><div class="notion-callout-text">注意：如果您更喜欢使用mocking库或由代码生成的mock代码，您也可以使用它们。 Go 允许您定义和实现小型接口，因此我们选择自己定义模拟，因为这是最简单的方法。</div></div><div class="notion-text notion-block-246155a4f89945fe998e98387fbcb551">下面的代码片段展示了如何使用注入的mock创建应用程序命令。</div><div class="notion-text notion-block-240c854ac1eb4036ba83e28531848639">请注意添加不检查任何相关内容的测试，这样您就不会最终测试模拟。如果没有业务逻辑，就完全跳过测试。我们已经覆盖了两个内层。我想到目前为止这似乎并不新鲜，因为我们都熟悉单元测试。然而，持续交付成熟度模型仅将它们列为“基本”成熟度级别。现在让我们看看集成测试。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-def3981e13e14908a0201969ba4b1fc9" data-id="def3981e13e14908a0201969ba4b1fc9"><span><div id="def3981e13e14908a0201969ba4b1fc9" class="notion-header-anchor"></div><a class="notion-hash-link" href="#def3981e13e14908a0201969ba4b1fc9" title="集成测试 "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">集成测试 </span></span></h3><div class="notion-text notion-block-3feceb5d567e4e7ba5f4c66917419dff">阅读此标题后，您是否想象过一个需要重试多次才能通过的长时间运行的测试？集成测试没有理由缓慢且不稳定。自动重试和增加睡眠时间等做法应该是绝对不可能的。</div><div class="notion-text notion-block-9e6ba685c98045fcaab604b2824ce5a7">在我们的上下文中，集成测试是<b>检查适配器是否与外部基础设施正常工作</b>的测试。大多数时候，这意味着测试数据库存储库。</div><div class="notion-text notion-block-b2b12bb5dd264073b20f8318354c2d2f">这些测试不是检查数据库是否正常工作，而是检查你是否正确使用它（集成部分）。这也是验证您是否知道如何使用数据库内部结构（例如处理事务）的绝佳方法。</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-3dab5e2020524157a08f4f575904f546"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:624px;max-width:100%;flex-direction:column"><img style="object-fit:cover" src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Fb769cf6b-c125-41d2-8f10-c232f7a47a38%2Faca89321-b279-43a9-93b1-1493e41d71a5%2FUntitled.png?table=block&amp;id=3dab5e20-2052-4157-a08f-4f575904f546" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-c636f64bbb7f40e1bd2d63d962924441">因为我们需要真正的基础设施，所以集成测试的编写和维护比单元测试更具挑战性。通常，我们可以使用 docker-compose 来启动所有依赖项。</div><div class="notion-callout notion-gray_background_co notion-block-8363b5c0008641148ed0e9a8072f2eb9"><div class="notion-page-icon-inline notion-page-icon-span"><span class="notion-page-icon" role="img" aria-label="💡">💡</span></div><div class="notion-callout-text">注意：我们是否应该使用 Docker 版本的数据库来测试我们的应用程序？ Docker 镜像几乎总是与我们在生产环境中运行的镜像略有不同。在某些情况下，例如 Firestore，只有模拟器可用，而不是真正的数据库。
事实上，Docker 并不能反映您在生产环境中运行的确切基础设施。然而，你更有可能搞乱代码中的 SQL 查询，而不是因为细微的配置差异而偶然发现问题。
一个好的做法是将镜像版本固定为与在生产环境中运行的版本相同。使用 Docker 不会为您提供 100% 的生产同等性，但它消除了“在我的机器上运行”问题并使用适当的基础设施测试您的代码。 Robert 在高质量数据库集成测试中深入介绍了数据库的集成测试。</div></div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-319945b94e7a4454bff3111c00008217" data-id="319945b94e7a4454bff3111c00008217"><span><div id="319945b94e7a4454bff3111c00008217" class="notion-header-anchor"></div><a class="notion-hash-link" href="#319945b94e7a4454bff3111c00008217" title="保持集成测试稳定和快速"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">保持集成测试稳定和快速</span></span></h4><div class="notion-text notion-block-84d2249a5d81402baa51bdde8ca524d9">当处理网络调用和数据库时，测试速度变得非常重要。并行运行测试至关重要，在 Go 中可以通过调用<code class="notion-inline-code"> t.Parallel() </code>启用并行测试。<b>这看起来很简单，但我们必须确保我们的测试支持这种行为</b>。</div><div class="notion-text notion-block-bfeeafbaae82455abbe4051524d6f50c">例如，考虑这个简单的测试场景： </div><ol start="1" class="notion-list notion-list-numbered notion-block-ac2cd1965e6a490bb69254baf231afde"><li>检查数据库中trainings集合是否为空</li></ol><ol start="2" class="notion-list notion-list-numbered notion-block-9ff2ae3007e44168b9eded5fbe508721"><li>调用添加训练的存储库方法</li></ol><ol start="3" class="notion-list notion-list-numbered notion-block-7bc3c012345a421bb3553c40d7e96957"><li>检查集合中是否有一项训练</li></ol><div class="notion-text notion-block-cb081d7fc70f4710ad67424418603704">如果另一个测试使用相同的集合，您将因竞争条件而随机失败。有时，该集合将包含多个我们刚刚添加的训练。</div><div class="notion-text notion-block-2033c2df7aa2499e9185195869cacc60">最简单的方法是永远不要断言列表长度之类的东西，而是检查它是否是我们正在测试的确切东西。例如，我们可以获取所有训练，然后迭代列表以检查是否存在预期的 ID。</div><div class="notion-text notion-block-59f336230de94efcb13de551aa04fe98">另一种方法是以某种方式隔离测试，这样它们就不会互相干扰。例如，每个测试用例都可以在唯一用户的上下文中工作（请参阅下面的组件测试）。</div><div class="notion-text notion-block-c01db6110b754e6286ebb0ba3da0bf6b">当然，这两种模式都比简单的长度断言更复杂。当您第一次偶然发现这个问题时，可能很容易放弃并决定“我们的集成测试不需要并行运行”。不要这样做。有时你需要发挥创造力，但最终并不需要付出太多努力。作为回报，您的集成测试将稳定且运行得与单元测试一样快。</div><div class="notion-text notion-block-e8dbca55df97400f9e46bf29d4e1be5f">如果您在每次运行之前创建了一个新数据库，这也是可以保证测试不相互干扰。</div><div class="notion-callout notion-gray_background_co notion-block-601288509c554105929ed648377cc0e1"><div class="notion-page-icon-inline notion-page-icon-span"><span class="notion-page-icon" role="img" aria-label="💡">💡</span></div><div class="notion-callout-text">警告！在迭代测试用例时，有些常见但难以发现的问题。
在使用表驱动测试时，您经常会看到如下代码：<div class="notion-text notion-block-507442603ab74f46be8275087bf0412f">这是对一部分测试用例运行测试的惯用方法。假设您现在想要并行运行每个测试用例。解决方案看起来很简单：</div><div class="notion-text notion-block-28496bd40776486d87d473dffa3f6f39">遗憾的是，这不会按预期工作。
添加并行开关使父测试函数不再等待 <code class="notion-inline-code">t.Run</code> 生成的子测试。因此，您<b>无法在 func 闭包内安全地使用 c 循环变量</b>。像这样运行测试通常会导致所有子测试都适用于最后一个测试用例，而忽略所有其他测试用例。
最糟糕的部分是测试将通过，并且在使用 <code class="notion-inline-code">-v </code>标志运行 go test 时您将看到正确的子测试名称。注意到这个问题的唯一方法是更改期望测试失败的代码并看到它们通过。正如 wiki 中提到的，解决此问题的一种方法是引入一个新的作用域变量：</div><div class="notion-text notion-block-e4a2edb7de1945c4ba0eefc3933177ae">这只是一个品味问题，但我们不喜欢这种方法，因为对于那些不知道这意味着什么的人来说，它看起来像是某种魔咒。相反，我们选择更冗长但明显的方法：</div><div class="notion-text notion-block-b54980db3d504ab48bf1ccc3df9b487c">即使您了解这种行为，也很容易滥用它，非常危险。更糟糕的是，似乎流行的 linter 默认情况下不会检查这一点 - 如果您知道做得很好的 linter，请在评论中分享。我们在 Watermillb 库中犯了这个错误，导致一些测试根本无法运行。您可以在此提交中看到修复</div></div></div><div class="notion-text notion-block-4f9b46fde63e42d2bb0c1dd52f8580ba">我们通过测试覆盖了数据库存储库，但我们还有一个 gRPC 客户端适配器。我们应该如何测试这个呢？</div><div class="notion-text notion-block-a5d575fe39a64700bf06b109120da85b">在这方面它与应用层类似。如果您的测试会重复它检查的代码，那么添加它可能没有意义。更改代码时只会增加额外的工作。让我们考虑一下用户服务 gRPC 适配器：</div><div class="notion-text notion-block-297ff6f093314fedbb7dda879aacc0d9">这里没有什么有趣的东西可以测试。我们可以注入一个模拟客户端并检查是否调用了正确的方法。但这不会验证任何内容，并且代码中的每个更改都需要在测试中进行相应的更改。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-8e1e565388264014b1827fdf7565b347" data-id="8e1e565388264014b1827fdf7565b347"><span><div id="8e1e565388264014b1827fdf7565b347" class="notion-header-anchor"></div><a class="notion-hash-link" href="#8e1e565388264014b1827fdf7565b347" title="组件测试 "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">组件测试 </span></span></h3><div class="notion-text notion-block-cab34d5cb83d4c64a41264adea9babc0">到目前为止，我们已经为应用程序的隔离部分创建了大部分狭窄的、专门的测试。此类测试非常适合检查极端情况和特定场景，但这并不意味着每个服务都能正常工作。很容易忘记从端口调用应用程序处理程序。此外，单独的单元测试并不能帮助我们确保应用程序在重大重构后仍然可以工作。</div><div class="notion-text notion-block-5ad1af182b554f31bfcd739e92d21737">现在是对我们所有服务运行端到端测试的时候了吗？还没有。</div><div class="notion-text notion-block-2606eee603964b6cab027cc070da6db4">由于没有调用测试类型的标准，我鼓励您遵循 Simon Stewart 的建议。创建一个表格，让团队中的每个人都清楚地了解特定测试的期望。然后，您可以删除有关该主题的所有（无成效的）讨论。在我们的例子中，该表可能如下所示：</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-bc3d98bda84e4d1185cb78750d9452c9"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:960px;max-width:100%;flex-direction:column"><img style="object-fit:cover" src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Fb769cf6b-c125-41d2-8f10-c232f7a47a38%2Fd73ebdb3-e0e8-4f8d-8f0e-b91861ff5b79%2FUntitled.png?table=block&amp;id=bc3d98bd-a84e-4d11-85cb-78750d9452c9" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-814a668a1d2d4f019552e6b1e01dc3c2">为了确保每个服务在内部正常工作，我们引入了组件测试来检查所有层的协同工作。组件测试涵盖与应用程序中的其他服务隔离的单个服务。</div><div class="notion-text notion-block-29dd940ff4bb429fbe720a6496eaee71">我们将调用真正的端口处理程序并使用 Docker 提供的基础设施，还将模拟所有到达外部服务的适配器。</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-5b98b3904b1543d7a30e0353a4c4fa44"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:100%;max-width:100%;flex-direction:column;height:100%"><img style="object-fit:cover" src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Fb769cf6b-c125-41d2-8f10-c232f7a47a38%2F5a833cd3-f19d-4f58-b9ac-11be9faf821c%2FUntitled.png?table=block&amp;id=5b98b390-4b15-43d7-a30e-0353a4c4fa44" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-f72db592d1814ebeb0a437245b27b4f9">您可能想知道，为什么不测试外部服务呢？毕竟，我们可以使用 Docker 容器并一起测试它们。</div><div class="notion-text notion-block-fb4b954fe08d4cf0aee2cf63a57f07da">问题在于测试多个连接服务的复杂性。如果你只有几个，那就足够了。但请记住，您需要为启动的每个服务配备适当的基础设施，包括它使用的所有数据库和它调用的所有外部服务。它很容易总共有数十个服务，通常由多个团队拥有。</div><div class="notion-text notion-block-9313be3d176e43e3b173545dbcb65b19">我们将在接下来的端到端测试中讨论这个问题。但现在，我们添加组件测试，因为我们需要一种快速的方法来了解服务是否正常工作。</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-5bfd798d79624e7db6b7ec311a2e65a8"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:816px;max-width:100%;flex-direction:column"><img style="object-fit:cover" src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Fb769cf6b-c125-41d2-8f10-c232f7a47a38%2Fa828d42a-b139-4645-90d9-6089ad814763%2FUntitled.png?table=block&amp;id=5bfd798d-7962-4e7d-b6b7-ec311a2e65a8" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-cadb4a929af44f62aa9adfc72f01ca24">我们可以在不需要集成环境的情况下完成大部分测试。我们可以并且确实独立于它所依赖的其他应用程序/服务来部署或发布我们的应用程序。</div><div class="notion-text notion-block-17396513faec4e918107a318e93d36e8">等下，微服务不是应该解决团队相互依赖的问题吗？如果您认为在您所处理的应用程序中不可能实现这一目标，则可能是因为架构选择不佳。您可以通过应用我们计划在未来章节中介绍的战略 DDD 模式来修复它。 我们在本系列中提出了这一点：<b>使用微服务并不会让您的应用程序和团队本身减少耦合。解耦需要对应用程序架构和系统级别进行有意识的设计</b>。在组件测试中，我们的目标是单独检查单个服务及其所需的所有基础设施的完整性。我们确保服务接受我们同意的 API 并以预期结果进行响应。</div><div class="notion-text notion-block-eaf967df26164e488a49c3f72fef3977">这些测试在技术上更具挑战性，但仍然相对简单。我们不会运行真正的服务二进制文件，因为我们需要模拟一些依赖项。我们必须修改启动服务的方式才能使其成为可能。</div><div class="notion-callout notion-gray_background_co notion-block-cf4584371fc94fb8b4c7a32ef216111a"><div class="notion-page-icon-inline notion-page-icon-span"><span class="notion-page-icon" role="img" aria-label="💡">💡</span></div><div class="notion-callout-text">注意 再次强调，如果您遵循依赖倒置原则（只是提醒一下，它是 SOLID 的一部分），那么在服务级别注入模拟应该是微不足道的。</div></div><div class="notion-text notion-block-c51c084b000a496c94efbf8678245054">我为我们的 <code class="notion-inline-code">app.Application</code> 结构引入了两个构造函数，它保存所有命令和查询。第一个的工作方式与以前一样，设置真正的 gRPC 客户端并注入它们。第二个用模拟代替它们。</div><div class="notion-text notion-block-a3d0fb2ca6a2462c90a2c5b99a321e50">我们现在可以简单地在单独的 goroutine 中运行该服务。</div><div class="notion-text notion-block-14867a2d2caf449ebb798f19430cb3d3">我们只想为所有测试运行一个服务实例，因此我们使用 <code class="notion-inline-code">TestMain</code> 函数。这是在运行测试之前插入设置的简单方法。</div><div class="notion-text notion-block-41091438981f475c98bc8511c6719695">我创建了 <code class="notion-inline-code">WaitForPort</code> 辅助函数，该函数会等待指定端口打开或超时。这很重要，因为您<b>需要确保服务已正确启动</b>。<b>不要用睡眠代替它</b>。您要么添加太多的延迟，使测试变慢，要么让它太短，并且它会随机失败。</div><div class="notion-text notion-block-a397499611c44a8cbc20feda8559cbc0">组件测试要测试什么？通常测试正常场景就足够了。不要检查那里的极端情况。单元和集成测试应该已经涵盖了这些。确保处理正确的有效负载、存储正常且响应正确。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-b2306fe033044790a86c8f514587f433" data-id="b2306fe033044790a86c8f514587f433"><span><div id="b2306fe033044790a86c8f514587f433" class="notion-header-anchor"></div><a class="notion-hash-link" href="#b2306fe033044790a86c8f514587f433" title="调用端口"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">调用端口</span></span></h3><div class="notion-text notion-block-725ed16ab9c74560b88faa48d003b3f0">我使用 <code class="notion-inline-code">openapi-codegen </code>生成的 HTTP 客户端。与服务器部分类似，这使得编写测试变得更加容易。例如，您不需要指定整个 REST 路径，也不需要每次都序列化JSON。</div><div class="notion-text notion-block-7d1d27fd65d34843b3e02c26ec181af7">尽管生成的客户端为我们节省了大量样板文件，但我仍然添加了带有客户端包装器的<code class="notion-inline-code">tests/client.go</code> 文件以用于测试目的。</div><div class="notion-text notion-block-20012b5c9ff84fbaaa27002b7d06199a">测试变得更具可读性，并且很容易理解发生了什么。让测试通过很容易，但在代码审查中理解它们、阅读所有断言和模拟要困难得多。</div><div class="notion-text notion-block-f74221ab25e344d3a44c5881ef24d449">我们现在可以使用一行来清楚地说明正在发生的事情，而不是上面的代码片段。</div><div class="notion-text notion-block-0353bb2eab7b4e429941065eeb7bd810">其他辅助方法有 <code class="notion-inline-code">FakeAttendeeJWT</code> 和 <code class="notion-inline-code">FakeTrainerJWT</code>。他们使用所选角色生成有效的授权token。由于 gRPC 使用从 protobuf 生成的结构，因此客户端已经很容易使用。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-b08f85d088f44c02a685f24cbdb595ce" data-id="b08f85d088f44c02a685f24cbdb595ce"><span><div id="b08f85d088f44c02a685f24cbdb595ce" class="notion-header-anchor"></div><a class="notion-hash-link" href="#b08f85d088f44c02a685f24cbdb595ce" title="端到端测试 "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">端到端测试 </span></span></h3><div class="notion-text notion-block-a07ff99b204744e693037c9bdf027f36">最后，我们来到测试套件中最可怕的部分。我们现在将把所有模拟抛在脑后。</div><div class="notion-text notion-block-7194318ed5994ee8a9c0963d97a7317b">端到端测试验证您的整个系统协同工作。它们速度慢、容易出错并且难以维护。您仍然需要它们，但请确保将它们构建得很好。</div><div class="notion-text notion-block-5a3e40ecca124cf5ba035b0eb8e312d6">在 Wild Workouts 中，它与运行组件测试类似，只是我们将启动 docker-compose 内的所有服务。然后，我们将验证一些仅调用 HTTP 端点的关键路径，因为这是我们向外部世界公开的内容。</div><div class="notion-callout notion-gray_background_co notion-block-cf57ee5b4d414be2941b77d4189ac611"><div class="notion-page-icon-inline notion-page-icon-span"><span class="notion-page-icon" role="img" aria-label="💡">💡</span></div><div class="notion-callout-text">注意：如果您无法在 docker-compose 上运行整个平台，则需要找到类似的方法。如果您已经在使用它，它可能是一个单独的 Kubernetes 集群或命名空间，或者某种临时环境。</div></div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-028da1ecbeb5476b82f02431dc72ff5b"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:864px;max-width:100%;flex-direction:column"><img style="object-fit:cover" src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Fb769cf6b-c125-41d2-8f10-c232f7a47a38%2F36d5d69d-b3bd-4260-82a6-794eac76fa10%2FUntitled.png?table=block&amp;id=028da1ec-beb5-476b-82f0-2431dc72ff5b" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-0c0f7dc938964f12a8e0cb85b575ef11">现在到了问题多于答案的部分。您应该在哪里进行端到端测试？哪个团队应该拥有它？在哪里运行它们？多频繁？它们应该是 CI/CD 管道的一部分还是从 cronjob 运行的单独的东西？</div><div class="notion-text notion-block-6ef5ac76eb24472f995fd4ef439daf1d">我无法给你明确的答案，因为这在很大程度上取决于你的团队结构、组织文化和 CI/CD 设置。与大多数挑战一样，尝试一种看起来最好的方法并迭代它，直到您满意为止。</div><div class="notion-text notion-block-3a356819730f4f93a1b6d7a34528ed53">我们很幸运，整个应用程序只有三个服务，所有服务都使用相同的数据库。随着服务和依赖项数量的增加，您的应用程序将变得更难以以这种方式进行测试。</div><div class="notion-text notion-block-684948414d00454ca55f55d778edb4ce">尽量保持端到端测试简短。他们应该<b>测试服务是否正确连接在一起，而不是测试它们内部的逻辑</b>。这些测试起到双重检查的作用。大多数时候他们不应该失败，如果失败，通常意味着有人违反了合同。</div><div class="notion-callout notion-gray_background_co notion-block-ba096878d2ca4a6d80482f5f1faedad2"><div class="notion-page-icon-inline notion-page-icon-span"><span class="notion-page-icon" role="img" aria-label="💡">💡</span></div><div class="notion-callout-text">请注意，我说过我们将仅使用 HTTP 端点，因为这是公开公开的。有一个例外：我们通过 gRPC 调用用户服务来为测试参与者添加训练课时余额。正如您所期望的，公众无法访问此端点。</div></div><div class="notion-text notion-block-a98a6905a0b7482191be391334671478">该代码与我们在组件测试中所做的非常接近。主要区别是我们不再在单独的 goroutine 中启动服务。相反，所有服务都使用我们在生产中部署的相同二进制文件在 docker-compose 内运行。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-0d25f50509be4fe6ad13f796f50ef637" data-id="0d25f50509be4fe6ad13f796f50ef637"><span><div id="0d25f50509be4fe6ad13f796f50ef637" class="notion-header-anchor"></div><a class="notion-hash-link" href="#0d25f50509be4fe6ad13f796f50ef637" title="验收测试 "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">验收测试 </span></span></h3><div class="notion-text notion-block-8649547e078b497189742833f1bc826a">您经常会发现验收测试被定义为单元测试和集成测试之后的下一个级别。我们认为它们与测试的技术方面是正交的。这是一个专注于完整业务功能而不是实现细节的测试。正如 Martin Fowler 所说：<div class="notion-text-children"><div class="notion-text notion-block-0976e3fe61794c5398752f690e75b51b">事情是这样的：在某一时刻，您应该确保从用户的角度（而不仅仅是从技术角度）测试您的软件是否正常工作。你所说的这些测试实际上并不那么重要。然而，进行这些测试是：选择一个术语，坚持使用它，然后编写这些测试。</div></div></div><div class="notion-text notion-block-6d64c78e7c0e42dcba990665d1187644">在我们的例子中，组件测试和端到端测试都可以被视为验收测试。如果您愿意，您可以对其中一些使用 BDD样式 - 这使它们更易于阅读，但添加了一些样板文件。</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-87fc7464adbe4751a0dcc2afbb954de6"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:624px;max-width:100%;flex-direction:column"><img style="object-fit:cover" src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Fb769cf6b-c125-41d2-8f10-c232f7a47a38%2Fba00e103-f671-4ced-b5d6-8af57252f8ae%2FUntitled.png?table=block&amp;id=87fc7464-adbe-4751-a0dc-c2afbb954de6" alt="notion image" loading="lazy" decoding="async"/></div></figure><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-5f86b9be96544ab5b6de7e42063eb403" data-id="5f86b9be96544ab5b6de7e42063eb403"><span><div id="5f86b9be96544ab5b6de7e42063eb403" class="notion-header-anchor"></div><a class="notion-hash-link" href="#5f86b9be96544ab5b6de7e42063eb403" title="我们现在可以睡个好觉了吗？"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">我们现在可以睡个好觉了吗？</span></span></h3><div class="notion-text notion-block-b13f0f4053ec42f783619582f0c7a0e4">除非我们在生产中破坏服务，否则它并没有真正经过测试。 </div><div class="notion-text notion-block-6b2fd61fd3b84d4a8501c4f1a7d9bd76">可靠的测试套件将捕获大部分错误，以便您能够一致地交付。但无论如何，您都希望为停电做好准备。我们现在进入监控、可观测性和混沌工程的主题。不过，这不在今天的讨论范围之内。</div><div class="notion-text notion-block-1d500417900f4520a4fa0ca2fb5a4ded">请记住，没有任何测试套件可以给您完全的信心。拥有一个允许快速回滚、恢复和撤消迁移的简单流程也至关重要。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-54542812fa934ba4ace40ca5e287d3ac" data-id="54542812fa934ba4ace40ca5e287d3ac"><span><div id="54542812fa934ba4ace40ca5e287d3ac" class="notion-header-anchor"></div><a class="notion-hash-link" href="#54542812fa934ba4ace40ca5e287d3ac" title="继续 "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">继续 </span></span></h3><div class="notion-text notion-block-55ce1ff617f7437f85e3b6f70371c28f">如果您查看完整的 commit，您可能会注意到我们现在注入依赖项的方式不是很优雅。我们将在未来迭代这一点。我们已经有了测试套件，但我们仍然错过了一个关键部分：<b>它没有在我们的持续集成管道中运行</b>。此外，为测试提供适当的 docker-compose 和环境变量也并非易事。当前的解决方案有效，但如果您不知道发生了什么，则不容易理解。在自动化构建中运行测试只会使其变得更加复杂。我们将在以后的章节中介绍这些主题。</div></main></div>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[【读书笔记】《Go With The Domain》10. 结合 DDD、CQRS 和简洁架构]]></title>
            <link>slw.ac.cn/article/go-with-the-domain-10.html</link>
            <guid>slw.ac.cn/article/go-with-the-domain-10.html</guid>
            <pubDate>Wed, 04 Oct 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[【关键词】整洁架构 DDD CQRS]]></description>
            <content:encoded><![CDATA[<div id="notion-article" class="mx-auto overflow-hidden "><main class="notion light-mode notion-page notion-full-width notion-block-3535635de37b46aa906382d25727c427"><div class="notion-viewport"></div><div class="notion-collection-page-properties"></div><div class="notion-text notion-block-8908706c0a10483f958e26363d940dd3">在前面的章节中，我们介绍了 DDD Lite、CQRS 和 Clean Architecture 等技术。即使单独使用它们是有益的，但它们一起使用效果最好。不幸的是，在实际项目中将它们一起使用并不容易。在本章中，我们将展示如何以最务实、最高效的方式连接 DDD Lite、CQRS 和 Clean Architecture。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-a3c819a37859456eb917c0cff2420349" data-id="a3c819a37859456eb917c0cff2420349"><span><div id="a3c819a37859456eb917c0cff2420349" class="notion-header-anchor"></div><a class="notion-hash-link" href="#a3c819a37859456eb917c0cff2420349" title="我为什么要在乎？"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">我为什么要在乎？</span></span></h3><div class="notion-text notion-block-f2b7e91b51c64807b87d847eb1fadf55">从事编程项目类似于规划和建设住宅区。如果您知道该地区将在不久的将来扩张，您需要为未来的改进保留空间。即使一开始它可能看起来像是浪费空间。您应该为住宅区、医院和寺庙等未来设施留出空间。否则，您将被迫摧毁建筑物和街道，为新建筑物腾出空间。早点考虑一下会好得多。</div><div class="notion-text notion-block-39bd19d0b3ca4eb98619c6ee9c86739f">情况和代码是一样的。如果您知道该项目的开发时间将超过 1 个月，那么您应该从一开始就牢记长远目标。您需要以不会妨碍您未来工作的方式创建代码。即使一开始看起来像是过度设计和大量额外的样板，您也需要牢记长远目标。</div><div class="notion-text notion-block-17a1ec95f7414d019f9ebed908f0a55e">这并不意味着您需要规划将来要实现的每一项功能——实际上恰恰相反。这种方法有助于适应新的要求或对我们领域不断变化的理解。这里不需要大的前期设计。这在当今时代至关重要，因为世界变化非常快，谁不能适应这些变化，就可能会被淘汰。</div><div class="notion-text notion-block-60371a1a08524ea7a89684c943c81072">这正是这些模式结合起来所给您带来的——<b>保持恒定开发速度的能力</b>。<b>无需过多破坏和触及现有代码</b>。</div><div class="notion-text notion-block-0c4d5b52f6e34def8be5433a5d6a123b">是否需要更多的思考和规划？这是一种更具挑战性的方式吗？您需要额外的知识才能做到这一点吗？当然！但从长远来看，这是值得的！幸运的是，您来对地方了。</div><div class="notion-text notion-block-b728065b6479455bb4375a1dbc15fc1d">但让我们把理论抛在脑后吧。让我们看代码。在本章中，我们将跳过设计选择的推理。我们已经在前面的章节中描述了这些。如果您还没有阅读它们，我建议您阅读一下——您会更好地理解本章。与前面的章节一样，我们的代码将基于重构真实的开源项目。这应该使示例更加现实并且适用于您的项目。</div><div class="notion-text notion-block-7c54c9a4e3d644f9ba5e2307e3dd9973">你准备好了吗？</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-674997a5bd924e3fbefe8658c1f01a81" data-id="674997a5bd924e3fbefe8658c1f01a81"><span><div id="674997a5bd924e3fbefe8658c1f01a81" class="notion-header-anchor"></div><a class="notion-hash-link" href="#674997a5bd924e3fbefe8658c1f01a81" title="让我们开始重构 "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">让我们开始重构 </span></span></h3><div class="notion-text notion-block-7782488b97ab427cb74995604078f2ea">让我们从 Domain-First 方法开始重构。我们将从领域层的介绍开始。因此，我们将确保实现细节不会影响我们的领域域代码。我们也可以全力以赴地<b>理解业务问题。不是编写无聊的数据库查询和 API 端点</b>。</div><div class="notion-text notion-block-a028cd21c5b74dd6a4e4d11a885a68b6">领域优先方法对于救援（重构）和新建项目都很有效。</div><div class="notion-text notion-block-532504d400714620a6fa625c530f2c29">为了开始构建我的领域层，我需要确定应用程序实际在做什么。本章将重点关注Wild Workouts 中 <code class="notion-inline-code">Trainings </code>微服务的重构。我首先确定应用程序处理的case。在之前重构为 Clean Architecture 之后。当我使用混乱的应用层时，我会查看 RPC 和 HTTP 端点以查找支持的case。</div><div class="notion-text notion-block-f9d99aced342455084233f250a0a34bd">我确定的功能之一是<b>批准重新安排训练</b>。在Wild Workouts中，如果在日期前 24 小时内提出重新安排训练的请求，则需要审批。如果参加者要求重新安排时间，则需要得到培训师的批准。当培训师提出要求时，学员需要接受。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-bb0de89a3d9341ddb6ff0047524b3186" data-id="bb0de89a3d9341ddb6ff0047524b3186"><span><div id="bb0de89a3d9341ddb6ff0047524b3186" class="notion-header-anchor"></div><a class="notion-hash-link" href="#bb0de89a3d9341ddb6ff0047524b3186" title="从领域开始"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">从领域开始</span></span></h3><div class="notion-text notion-block-885f0bdc56b74843b06c9a462dd21849">即使它看起来不像您一生中见过的最糟糕的代码，但像 <code class="notion-inline-code">ApproveTrainingReschedule </code>这样的函数随着时间的推移往往会变得更加复杂。更复杂的功能意味着未来开发过程中潜在的错误更多。</div><div class="notion-text notion-block-5354cfb8dea9476f98ab2145df23cd1a">如果我们是这个项目的新手，而且我们没有关于它的“萨满知识”，那么这种情况就更有可能发生。<b>您应该始终考虑在您之后将从事该项目的所有人员，并使其能够防止他们意外破坏项目</b>。这将有助于您的项目不会成为每个人都不敢触碰的遗产。当你刚接触这个项目时，你可能讨厌这种感觉，并且你害怕接触任何东西以免破坏系统。</div><div class="notion-text notion-block-63644469a0f9432db2fdba3a7bea4f86">人们每两年换一次工作的频率并不少见。这使得它对于长期项目开发变得更加重要。</div><div class="notion-text notion-block-fa39cf3a852d4830b1a61b6a2adfb04d">如果您不认为这段代码可能会变得复杂，我建议您检查您所从事的项目中最糟糕的地方的 Git 历史记录。在大多数情况下，最糟糕的代码都是以“几个简单的 if”开始的。代码越复杂，以后简化起来就越困难。我们应该<b>对新出现的复杂性保持敏感，并尽快尝试将其简化</b>。</div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-1000309277c447e081ed7447399d9a8c" data-id="1000309277c447e081ed7447399d9a8c"><span><div id="1000309277c447e081ed7447399d9a8c" class="notion-header-anchor"></div><a class="notion-hash-link" href="#1000309277c447e081ed7447399d9a8c" title="训练领域实体entity"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">训练领域实体entity</span></span></h4><div class="notion-text notion-block-cd7b2d2a9702435a9d7c9d992a8f3f4f">在分析训练微服务处理的当前用例时，我发现它们都与训练相关。创建一个 Training 类型来处理这些操作是很自然的。</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-a86170aa3eb7418ca1ec369bde0355c8"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:661px;max-width:100%;flex-direction:column"><img style="object-fit:cover" src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Fb769cf6b-c125-41d2-8f10-c232f7a47a38%2F7d16b273-f64c-496c-be63-f9ce628754c8%2FUntitled.png?table=block&amp;id=a86170aa-3eb7-418c-a1ec-369bde0355c8" alt="重构TrainingService的方法" loading="lazy" decoding="async"/><figcaption class="notion-asset-caption">重构TrainingService的方法</figcaption></div></figure><div class="notion-callout notion-gray_background_co notion-block-c5f6e815857b4f52a3be7908cd6a079a"><div class="notion-page-icon-inline notion-page-icon-span"><span class="notion-page-icon" role="img" aria-label="💡">💡</span></div><div class="notion-callout-text">名词==实体 ？
这是发现实体的有效方法吗？嗯，不是真的。
 DDD 提供的工具可以帮助我们在无需猜测的情况下对复杂领域进行建模（战略 DDD 模式，聚合）。我们不想猜测我们的聚合是什么样子——我们希望有工具来发现它们。事件风暴技术在这里非常有用……但它是一个单独章节的主题。
这个主题非常复杂，需要用几章来讨论。这就是我们很快要做的事情。
这是否意味着在没有战略 DDD 模式的情况下不应使用这些技术？当然不是！当前的方法对于更简单的项目来说已经足够了。不幸的是（或者幸运的是），并非所有项目都是简单的。</div></div><div class="notion-blank notion-block-82fe81bcbe6248a793f1ee44c1b8733f"> </div><div class="notion-text notion-block-222f8af157b44f10988a1cda3619d25b"><b>所有字段都是私有的</b>以提供封装。这对于满足 DDD Lite 章节中的“始终在内存中保持有效状态” 规则至关重要。</div><div class="notion-text notion-block-02001ee4791446cdbbb565bba3a8a810">由于构造函数和封装字段中的验证，我们确信 <code class="notion-inline-code">Training </code>始终有效。现在，对项目没有任何了解的人无法以错误的方式使用它。同样的规则适用于<code class="notion-inline-code">Training</code>提供的任何方法。</div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-12568e3542704e87b258806c50969bf6" data-id="12568e3542704e87b258806c50969bf6"><span><div id="12568e3542704e87b258806c50969bf6" class="notion-header-anchor"></div><a class="notion-hash-link" href="#12568e3542704e87b258806c50969bf6" title="在领域层审批"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">在领域层审批</span></span></h4><div class="notion-text notion-block-1cea0eff6a1f4b228e874b5757a85aef">正如 DDD Lite 简介中所述，我们使用<b>面向行为</b>的方法构建我们的领域。不是靠数据。让我们在领域实体上对 <code class="notion-inline-code">ApproveReschedule </code>进行建模</div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-e18c0f17429a41398f729168425aa508" data-id="e18c0f17429a41398f729168425aa508"><span><div id="e18c0f17429a41398f729168425aa508" class="notion-header-anchor"></div><a class="notion-hash-link" href="#e18c0f17429a41398f729168425aa508" title="使用命令编排 "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">使用命令编排 </span></span></h4><div class="notion-text notion-block-d489213a3a21448eb102847360ccb92c">现在应用层可以只负责流程的编排。那里没有领域逻辑。我们将整个业务复杂性隐藏在领域层中。这正是我们的目标。为了获取和保存训练，我们使用存储库模式。</div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-d27a451c00914c1694af3997873a67f8" data-id="d27a451c00914c1694af3997873a67f8"><span><div id="d27a451c00914c1694af3997873a67f8" class="notion-header-anchor"></div><a class="notion-hash-link" href="#d27a451c00914c1694af3997873a67f8" title="取消训练的重构"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">取消训练的重构</span></span></h4><div class="notion-text notion-block-866bc58b3b2245af85c6967a35b63b20">现在让我们看一下 <code class="notion-inline-code">TrainingService </code>中的 <code class="notion-inline-code">CancelTraining</code>。</div><div class="notion-text notion-block-78500d4d7d8f4b52a520ac8b3b44d455">那里的领域逻辑很简单：您可以在训练日期前 24 小时取消训练。如果距培训时间不到 24 小时，但您无论如何都想取消培训：</div><ul class="notion-list notion-list-disc notion-block-0eb717b853bf4c4a81fb45d2f7e44bfd"><li>如果您是教练，学员将被取消训练，并额外参加一次课程（没有人喜欢在同一天改变计划！）</li></ul><ul class="notion-list notion-list-disc notion-block-54b52be063b64a9f96b90223459477ea"><li>如果您是学员，您将失去本次训练</li></ul><div class="notion-text notion-block-2c200e116a25463e8f999ecb711e9c2d">当前的实施方式如下：</div><div class="notion-text notion-block-bd3f888df97448a9ba1f27fc32356f8e">您可以看到某种用于在取消期间计算训练余额增量的“算法”。这对应用层来说不是一个好兆头。</div><div class="notion-text notion-block-ca11a32610bf47978567ff3f17ad97c3">像这样的逻辑应该存在于我们的领域层中。<b>如果您开始在应用程序层中看到一些与逻辑相关的 if，您应该考虑如何将其移动到领域层。</b>在其他地方测试和重用会更容易。</div><div class="notion-text notion-block-849b62113b93410e99829b4403166cfe">这可能取决于项目，但通常领域逻辑在初始开发后相当稳定，并且可以长期保持不变。它可以在服务、框架更改、库更改和 API 更改之间移动。</div><div class="notion-text notion-block-1fd84522d65b4427bc92e6db715a1d1e">由于这种分离，我们可以以更安全、更快速的方式进行所有这些改变。让我们将 <code class="notion-inline-code">CancelTraining </code>方法分解为多个独立的部分。这将使我们能够独立测试和更改它们。</div><div class="notion-text notion-block-2e8852c7820149baab38482ad24d51ff">首先，我们需要处理取消逻辑并将训练标记为已取消。</div><div class="notion-text notion-block-89d75dbe0ea743adbadcc098b638142c">这里没有什么真正复杂的。那挺好的！</div><div class="notion-text notion-block-0f7c78e71acb4ddaa1d932149bad934b">第二个需要移动的部分是计算取消后训练余额的“算法”。理论上，我们可以将其放入 <code class="notion-inline-code">Cancel()</code> 方法中，但在我看来，这会打破单一职责原则 和 CQRS 。而且我喜欢小功能。</div><div class="notion-text notion-block-ea5a09eaa4464291b9faf9dfa64aeb9a">但该放在哪里呢？一些对象？领域服务？在某些语言中，比如以 J 开头并以 ava 结尾的语言，这是有意义的。但在 Go 中，只需创建一个简单的函数就足够了。</div><div class="notion-text notion-block-17d6e9d889bc49dbb2c8a294507ebb81">代码现在很简单了。我可以想象我可以和任何非技术人员坐在一起，通过这段代码来解释它是如何工作的。</div><div class="notion-text notion-block-3a5ed42b6de34557adfd900a00463b02">测试怎么样？这可能有点争议。测试代码将复制该函数的实现。计算算法的任何更改都需要将逻辑复制到测试中。我不会在那里写测试，但如果你想晚上睡得更好——为什么不呢！</div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-d8f2d146a890439b9f2d586ff107253f" data-id="d8f2d146a890439b9f2d586ff107253f"><span><div id="d8f2d146a890439b9f2d586ff107253f" class="notion-header-anchor"></div><a class="notion-hash-link" href="#d8f2d146a890439b9f2d586ff107253f" title="将 CancelTraining 移至命令 "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">将 CancelTraining 移至命令 </span></span></h4><div class="notion-text notion-block-c70a484f48f64cb7bf3246d63bb6da69">我们的领域已准备就绪，现在让我们使用它。我们将按照与之前相同的方式进行操作：</div><ol start="1" class="notion-list notion-list-numbered notion-block-e0892f4205424075842cdc0950db018f"><li>从存储库获取实体</li></ol><ol start="2" class="notion-list notion-list-numbered notion-block-41f6a74aa4884bdbb57628588706c7db"><li>编排领域内容</li></ol><ol start="3" class="notion-list notion-list-numbered notion-block-c01e96ca1b7e4f42af4da4ca6fd82121"><li>调用外部<code class="notion-inline-code">trainer</code>服务取消培训（此服务是“教练日历”的关键点） )</li></ol><ol start="4" class="notion-list notion-list-numbered notion-block-25cfa7760149409bbfe2682a66435923"><li>返回要保存在数据库中的实体。</li></ol><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-62f5acd7828744fe844880c591044a12" data-id="62f5acd7828744fe844880c591044a12"><span><div id="62f5acd7828744fe844880c591044a12" class="notion-header-anchor"></div><a class="notion-hash-link" href="#62f5acd7828744fe844880c591044a12" title="存储库重构 "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">存储库重构 </span></span></h3><div class="notion-text notion-block-5b9b170f9f184c21bb0b9b3011fe15ad">存储库的初始实现非常棘手，因为每个用例都有自定义方法。</div><div class="notion-text notion-block-5b0ce0de07d34fd4a9c5a3c414d2b665">由于引入了<code class="notion-inline-code">training.Training</code>实体，我们可以拥有一个更简单的版本，其中一种方法用于添加新训练，另一种方法用于更新。</div><div class="notion-text notion-block-bb5020d8ea484cb49e7c4f0f2814f7eb">正如存储库模式中一样，我们使用 Firestore 实现了存储库。我们还将在当前的实现中使用 Firestore。请记住，这是一个实现细节 - 您可以使用任何您想要的数据库。在上一章中，我们展示了使用不同数据库的示例实现。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-3eabd68567e24c998450d5c7b68f131c" data-id="3eabd68567e24c998450d5c7b68f131c"><span><div id="3eabd68567e24c998450d5c7b68f131c" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3eabd68567e24c998450d5c7b68f131c" title="连接一切"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">连接一切</span></span></h3><div class="notion-text notion-block-2ab19eb3c22341bf900910abc7fed913"> 现在如何使用我们的代码？我们的端口层呢？感谢 Miłosz 在《整洁架构》中所做的重构，我们的端口层与其他层解耦。这就是为什么在这次重构之后，它几乎不需要任何重大改变。我们只是调用应用程序命令而不是应用程序服务。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-8150896296b1416597a975349c0449e9" data-id="8150896296b1416597a975349c0449e9"><span><div id="8150896296b1416597a975349c0449e9" class="notion-header-anchor"></div><a class="notion-hash-link" href="#8150896296b1416597a975349c0449e9" title="在实际项目中如何进行这样的重构？"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">在实际项目中如何进行这样的重构？</span></span></h3><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-bdef803c0d7b46aa9dba6a41b701a400"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:528px;max-width:100%;flex-direction:column"><img style="object-fit:cover" src="https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Fb769cf6b-c125-41d2-8f10-c232f7a47a38%2Fab4708fc-23eb-431f-b2ce-125e80e2383c%2FUntitled.png?table=block&amp;id=bdef803c-0d7b-46aa-9dba-6a41b701a400" alt="整洁架构分层" loading="lazy" decoding="async"/><figcaption class="notion-asset-caption">整洁架构分层</figcaption></div></figure><div class="notion-text notion-block-2ed393971e7c4e53893ae507142bfe36">如何在实际项目中进行此类重构可能并不明显。进行代码审查并在团队层面就重构方向达成一致是很困难的。</div><div class="notion-text notion-block-46f21959660c4678a703aeacd72b245e">根据我的经验，最好的方法是 Pair或 Mob编程。就算一开始，你可能会觉得浪费时间，知识分享和即时复习会在以后节省很多时间。得益于丰富的知识共享，您可以在初始项目或重构阶段之后更快地工作。</div><div class="notion-text notion-block-d34c9fb6105e47cc8fc8c64c8e21c74f">在这种情况下，您不应该考虑 Mob/Pair 编程所损失的时间。你应该考虑一下因为不做而可能损失的时间。它还将帮助您更快地完成重构，因为您无需等待决策。您可以立即就它们达成一致。在实施复杂的新建项目时，群体编程和结对编程也能完美地工作。</div><div class="notion-text notion-block-80aac945a98642fa9c170125ce1e3435">在这种情况下，知识共享尤其重要。我多次看到这种方法如何让项目在长期内进展得非常快。当你进行重构时，就合理的时间范围达成一致也很重要。并保留它们。</div><div class="notion-text notion-block-23f2a87116df4714979de72858f512c0">当您花费整整一个月的时间进行重构时，您可能很快就会失去利益相关者的信任，并且改进并不明显。尽快集成和部署重构也很重要。完美，每天（如果您可以在非重构工作中做到这一点，我相信您也可以在重构中做到这一点！）。如果您的更改长时间未合并和未部署，则会增加破坏功能的机会。它还会阻止重构服务中的任何工作或使更改更难以合并（并不总是能够停止周围的所有其他开发）。</div><div class="notion-text notion-block-2e5404074d654a98bc29f8c5fcf870cb">但是什么时候知道项目是否复杂到可以使用 mob 编程呢？不幸的是，没有神奇的公式可以做到这一点。但您应该问自己一些问题：</div><ul class="notion-list notion-list-disc notion-block-6e626cccc8134b569b6d0254a0909b30"><li>我们了解该领域吗？</li></ul><ul class="notion-list notion-list-disc notion-block-e5322f77c94b492dabdbb9a56c3f1322"><li>我们知道如何实施吗？</li></ul><ul class="notion-list notion-list-disc notion-block-95fe6a87bca845beab36c05c88509db3"><li>最终会产生一个无人能审查的巨大拉取请求吗？</li></ul><ul class="notion-list notion-list-disc notion-block-46e207008a6b42058a6b3c366d3f2151"><li>我们是否可以在不进行群体/结对编程的情况下冒更糟糕的知识共享风险？</li></ul><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-0e5af32b70b94bf28e76d16161e7b289" data-id="0e5af32b70b94bf28e76d16161e7b289"><span><div id="0e5af32b70b94bf28e76d16161e7b289" class="notion-header-anchor"></div><a class="notion-hash-link" href="#0e5af32b70b94bf28e76d16161e7b289" title="总结 "><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">总结 </span></span></h3><div class="notion-text notion-block-f545bc02ddbe43c7bafa5594f2c2eb02">我们到此结束。重构的完整差异可以在我们的 Wild Workouts GitHub上找到（注意，它很大！）。我希望在本章之后，您还会看到所有引入的模式如何很好地协同工作。如果还没有，请不要担心。我花了三年时间才把所有的点串联起来。但所花的时间是值得的。在我了解了一切是如何相互联系之后，我开始以完全不同的方式看待新项目。从长远来看，它使我和我的团队能够更高效地工作。还值得一提的是，与所有技术一样，这种组合并不是灵丹妙药。如果您正在创建的项目并不复杂，并且在开发 1 个月后不会很快被触及，那么将所有内容放入一个主包中可能就足够了。不过要注意，这 1 个月的开发时间很可能将变成一年！</div><div class="notion-blank notion-block-47f62694b4724e59a70b2f7d734044c9"> </div></main></div>]]></content:encoded>
        </item>
    </channel>
</rss>