summaryrefslogtreecommitdiff
path: root/2025
diff options
context:
space:
mode:
Diffstat (limited to '2025')
-rw-r--r--2025/02/10/实时搜索中的防抖函数/index.html10
-rw-r--r--2025/02/18/Prisma-关系型数据库的-Self-relations/index.html2
-rw-r--r--2025/03/13/领域驱动设计中的“聚合根”/index.html12
-rw-r--r--2025/03/14/DDD-中的-Ubiquitous-Languages/index.html16
-rw-r--r--2025/03/19/领域驱动设计中聚合根持久化和事件发布可能导致数据不一致问题/index.html2
-rw-r--r--2025/03/20/TDD-和-DDD-的一些小想法/index.html30
-rw-r--r--2025/03/20/肺功能检查数值/index.html40
-rw-r--r--2025/03/25/Scala-3-Capture-Checking/index.html72
-rw-r--r--2025/03/31/在F-中处理复杂依赖注入的实践指南/index.html26
-rw-r--r--2025/04/02/N-1-selects-problem-与-Prisma-ORM/index.html26
-rw-r--r--2025/04/13/uuidv7-rdbms/index.html2
-rw-r--r--2025/05/07/nestjs-bullmq-mail-business/index.html16
-rw-r--r--2025/05/08/Multiplayer-Collaborative-Systems-tips/index.html8
-rw-r--r--2025/05/27/vertical-slicing-practice/index.html44
14 files changed, 153 insertions, 153 deletions
diff --git a/2025/02/10/实时搜索中的防抖函数/index.html b/2025/02/10/实时搜索中的防抖函数/index.html
index 168f08af..9e826d10 100644
--- a/2025/02/10/实时搜索中的防抖函数/index.html
+++ b/2025/02/10/实时搜索中的防抖函数/index.html
@@ -194,9 +194,9 @@
<div class="post-content">
<p>在实现实时搜索功能时,通常会使用输入框的事件监听器来捕获用户的输入变化,并在输入变化时发送搜索请求。为了避免过多的请求导致服务器负担过重,通常会使用“防抖”(debounce)技术来控制请求的频率。</p>
<h3 id="实现步骤"><a href="#实现步骤" class="headerlink" title="实现步骤"></a>实现步骤</h3><ol>
-<li><strong>监听输入框的变化</strong>:使用<code>input</code>事件监听器来捕获用户的输入变化。</li>
-<li><strong>防抖处理</strong>:使用防抖函数来限制请求的频率。防抖函数会在用户停止输入一段时间后才发送请求。</li>
-<li><strong>发送请求</strong>:在防抖函数中调用搜索请求。</li>
+<li>监听输入框的变化:使用<code>input</code>事件监听器来捕获用户的输入变化。</li>
+<li>防抖处理:使用防抖函数来限制请求的频率。防抖函数会在用户停止输入一段时间后才发送请求。</li>
+<li>发送请求:在防抖函数中调用搜索请求。</li>
</ol>
<h3 id="防抖函数示例"><a href="#防抖函数示例" class="headerlink" title="防抖函数示例"></a>防抖函数示例</h3><p>以下是一个简单的防抖函数示例:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">debounce</span>(<span class="params">func, wait</span>) &#123;</span><br><span class="line"> <span class="keyword">let</span> timeout;</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">function</span>(<span class="params">...args</span>) &#123;</span><br><span class="line"> <span class="keyword">const</span> context = <span class="variable language_">this</span>;</span><br><span class="line"> <span class="built_in">clearTimeout</span>(timeout);</span><br><span class="line"> timeout = <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> func.<span class="title function_">apply</span>(context, args), wait);</span><br><span class="line"> &#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
@@ -205,8 +205,8 @@
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line">&lt;script&gt;</span><br><span class="line"> import &#123; onMount &#125; from &#x27;svelte&#x27;;</span><br><span class="line"> import &#123; patientsStore &#125; from &#x27;$lib/stores/patients.svelte&#x27;;</span><br><span class="line"></span><br><span class="line"> let searchTerm = &#x27;&#x27;;</span><br><span class="line"></span><br><span class="line"> // 防抖函数</span><br><span class="line"> function debounce(func, wait) &#123;</span><br><span class="line"> let timeout;</span><br><span class="line"> return function(...args) &#123;</span><br><span class="line"> const context = this;</span><br><span class="line"> clearTimeout(timeout);</span><br><span class="line"> timeout = setTimeout(() =&gt; func.apply(context, args), wait);</span><br><span class="line"> &#125;;</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> // 搜索函数</span><br><span class="line"> const searchPatients = debounce(async (term) =&gt; &#123;</span><br><span class="line"> if (term) &#123;</span><br><span class="line"> // 发送搜索请求</span><br><span class="line"> const response = await fetch(`/api/search-patients?query=$&#123;term&#125;`);</span><br><span class="line"> const data = await response.json();</span><br><span class="line"> patientsStore.mbglPatients = data;</span><br><span class="line"> &#125; else &#123;</span><br><span class="line"> // 清空搜索结果或恢复默认数据</span><br><span class="line"> patientsStore.mbglPatients = [];</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;, 300); // 300ms 的防抖时间</span><br><span class="line"></span><br><span class="line"> // 监听输入框变化</span><br><span class="line"> function handleInput(event) &#123;</span><br><span class="line"> searchTerm = event.target.value;</span><br><span class="line"> searchPatients(searchTerm);</span><br><span class="line"> &#125;</span><br><span class="line">&lt;/script&gt;</span><br><span class="line"></span><br><span class="line">&lt;input type=&quot;text&quot; placeholder=&quot;搜索患者...&quot; on:input=&#123;handleInput&#125; bind:value=&#123;searchTerm&#125; /&gt;</span><br></pre></td></tr></table></figure>
<h3 id="请求发送间隔"><a href="#请求发送间隔" class="headerlink" title="请求发送间隔"></a>请求发送间隔</h3><ul>
-<li><strong>防抖时间</strong>:通常设置为 300ms 到 500ms 之间。这个时间足够让用户完成输入并减少不必要的请求。</li>
-<li><strong>考虑用户体验</strong>:防抖时间过短可能导致过多请求,过长则可能让用户感到延迟。300ms 是一个常用的折中值。</li>
+<li>防抖时间:通常设置为 300ms 到 500ms 之间。这个时间足够让用户完成输入并减少不必要的请求。</li>
+<li>考虑用户体验:防抖时间过短可能导致过多请求,过长则可能让用户感到延迟。300ms 是一个常用的折中值。</li>
</ul>
<p>通过这种方式,你可以实现一个高效的实时搜索功能,既能保证用户体验,又能减少服务器的负担。</p>
diff --git a/2025/02/18/Prisma-关系型数据库的-Self-relations/index.html b/2025/02/18/Prisma-关系型数据库的-Self-relations/index.html
index b69d4397..7a025e10 100644
--- a/2025/02/18/Prisma-关系型数据库的-Self-relations/index.html
+++ b/2025/02/18/Prisma-关系型数据库的-Self-relations/index.html
@@ -207,7 +207,7 @@
<ul>
<li>关系的两端都必须定义一个共享相同名称的 <code>@relation</code> 属性(<code>BlogOwnerHistory</code>)</li>
<li>关系字段必须是<a target="_blank" rel="noopener" href="https://www.prisma.io/docs/orm/prisma-schema/data-model/relations#relation-fields">完全注释</a>的。例如 <code>successor</code> 字段需要定义 <code>field</code> 和 <code>references</code> 参数。</li>
-<li>关系字段必须由外键支持。<code>successor</code> 字段由 <code>successorId</code> 外键提供支持,该外键引用 <code>id</code> 字段中的值。<code>successorId</code> 还需要 <code>@unique</code> 属性来保证一对一的关系。****</li>
+<li>关系字段必须由外键支持。<code>successor</code> 字段由 <code>successorId</code> 外键提供支持,该外键引用 <code>id</code> 字段中的值。<code>successorId</code> 还需要 <code>@unique</code> 属性来保证一对一的关系。</li>
</ul>
<blockquote>
<p>一对一的 self-relation 需要两个端点,即使这两个端点是同一条数据。</p>
diff --git a/2025/03/13/领域驱动设计中的“聚合根”/index.html b/2025/03/13/领域驱动设计中的“聚合根”/index.html
index 56df60c9..be156f43 100644
--- a/2025/03/13/领域驱动设计中的“聚合根”/index.html
+++ b/2025/03/13/领域驱动设计中的“聚合根”/index.html
@@ -195,15 +195,15 @@
<p>在领域驱动设计(Domain-Driven Design,简称DDD)中,聚合根(Aggregate Root)是聚合(Aggregate)中的核心实体,是一个聚合的入口点和控制者,负责维护聚合内部的一致性和不变性条件。聚合是一组紧密相关的领域对象的集合,这些对象通过一定的业务规则绑定在一起,并被视为一个单元。</p>
<p>主要的作用如下:</p>
<ul>
-<li><strong>维护不变性</strong>:聚合根确保聚合内所有对象的一致性和不变性条件不被破坏。它负责封装与聚合相关的业务逻辑,保证聚合内的对象符合业务规则。</li>
-<li><strong>管理生命周期</strong>:聚合根负责管理其内部对象的创建、修改和删除。它控制着聚合内部成员的生命周期,包括它们的创建、更新和删除。</li>
-<li><strong>处理业务逻辑</strong>:聚合根负责处理与聚合相关的业务逻辑和操作,外部系统通过调用聚合根的方法来执行这些操作。它不仅是数据的容器,还负责封装与聚合相关的业务逻辑。</li>
+<li>维护不变性:聚合根确保聚合内所有对象的一致性和不变性条件不被破坏。它负责封装与聚合相关的业务逻辑,保证聚合内的对象符合业务规则。</li>
+<li>管理生命周期:聚合根负责管理其内部对象的创建、修改和删除。它控制着聚合内部成员的生命周期,包括它们的创建、更新和删除。</li>
+<li>处理业务逻辑:聚合根负责处理与聚合相关的业务逻辑和操作,外部系统通过调用聚合根的方法来执行这些操作。它不仅是数据的容器,还负责封装与聚合相关的业务逻辑。</li>
</ul>
<p>其具有以下特性:</p>
<ul>
-<li><strong>唯一入口</strong>:聚合根是聚合内部对象的唯一入口,外部系统只能与聚合根交互,而无法直接访问聚合内部的其他对象。这样可以避免外部系统直接修改聚合内的实体,确保聚合的一致性和业务逻辑的完整性。</li>
-<li><strong>标识唯一性</strong>:每个聚合根都有一个全局唯一的标识符(ID),用以区分不同的聚合实例。</li>
-<li><strong>事务边界</strong>:聚合根常常作为事务的边界,确保事务内的所有操作要么全部成功,要么全部失败,以此来维护数据的完整性。</li>
+<li>唯一入口:聚合根是聚合内部对象的唯一入口,外部系统只能与聚合根交互,而无法直接访问聚合内部的其他对象。这样可以避免外部系统直接修改聚合内的实体,确保聚合的一致性和业务逻辑的完整性。</li>
+<li>标识唯一性:每个聚合根都有一个全局唯一的标识符(ID),用以区分不同的聚合实例。</li>
+<li>事务边界:聚合根常常作为事务的边界,确保事务内的所有操作要么全部成功,要么全部失败,以此来维护数据的完整性。</li>
</ul>
<p>用 F# 来描述,以订单管理为例,大概写一下:</p>
<figure class="highlight fsharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">OrderStatus</span> <span class="operator">=</span> </span><br><span class="line"> <span class="operator">|</span> New</span><br><span class="line"> <span class="operator">|</span> Shipped</span><br><span class="line"> <span class="operator">|</span> Delivered</span><br><span class="line"> <span class="operator">|</span> Cancelled</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">OrderItem</span> (productName<span class="operator">:</span> <span class="type">string</span>, price<span class="operator">:</span> <span class="type">float</span>, quantity<span class="operator">:</span> <span class="type">int</span>) <span class="operator">=</span> </span><br><span class="line"> <span class="keyword">do</span></span><br><span class="line"> <span class="keyword">if</span> quantity <span class="operator">&lt;=</span> <span class="number">0</span> <span class="keyword">then</span></span><br><span class="line"> <span class="built_in">failwith</span> <span class="string">&quot;Quantity must be positive&quot;</span></span><br><span class="line"> </span><br><span class="line"> <span class="keyword">member</span> <span class="keyword">public</span> this.ProductName <span class="operator">=</span> productName</span><br><span class="line"> <span class="keyword">member</span> <span class="keyword">public</span> this.Price <span class="operator">=</span> price</span><br><span class="line"> <span class="keyword">member</span> <span class="keyword">public</span> this.Quantity <span class="operator">=</span> quantity</span><br><span class="line"> <span class="keyword">member</span> <span class="keyword">public</span> this.TotalPrice () <span class="operator">=</span> price <span class="operator">*</span> quantity</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Order</span> (<span class="built_in">id</span><span class="operator">:</span> <span class="type">int</span>, customerName<span class="operator">:</span> <span class="type">string</span>) <span class="operator">=</span> </span><br><span class="line"> <span class="keyword">let</span> <span class="keyword">mutable</span> status <span class="operator">=</span> OrderStatus.New</span><br><span class="line"> <span class="keyword">let</span> <span class="keyword">mutable</span> orderItems <span class="operator">=</span> []</span><br><span class="line"></span><br><span class="line"> <span class="keyword">member</span> <span class="keyword">public</span> this.Id <span class="operator">=</span> <span class="built_in">id</span></span><br><span class="line"> <span class="keyword">member</span> <span class="keyword">public</span> this.CustomerName <span class="operator">=</span> customerName</span><br><span class="line"> <span class="keyword">member</span> <span class="keyword">public</span> this.Status <span class="operator">=</span> status</span><br><span class="line"> <span class="keyword">member</span> <span class="keyword">public</span> this.OrderItems <span class="operator">=</span> orderItems</span><br><span class="line"></span><br><span class="line"> <span class="keyword">member</span> <span class="keyword">public</span> this.AddItem (item<span class="operator">:</span> OrderItem, price<span class="operator">:</span> <span class="type">float</span>, quantity<span class="operator">:</span> <span class="type">int</span>) <span class="operator">=</span></span><br><span class="line"> <span class="keyword">if</span> quantity <span class="operator">&lt;=</span> <span class="number">0</span> <span class="keyword">then</span></span><br><span class="line"> <span class="built_in">failwith</span> <span class="string">&quot;Quantity must be positive&quot;</span></span><br><span class="line"> </span><br><span class="line"> orderItems <span class="operator">&lt;-</span> orderItems <span class="operator">@</span> [OrderItem(item.ProductName, price, quantity)]</span><br><span class="line"></span><br><span class="line"> <span class="keyword">member</span> <span class="keyword">public</span> this.ChangeStatus (status<span class="operator">:</span> OrderStatus) <span class="operator">=</span></span><br><span class="line"> this.Status <span class="operator">&lt;-</span> status</span><br><span class="line"></span><br><span class="line"> <span class="keyword">member</span> <span class="keyword">public</span> this.TotalPrice () <span class="operator">=</span></span><br><span class="line"> orderItems <span class="operator">|&gt;</span> List.sumBy (<span class="keyword">fun</span> item <span class="operator">-&gt;</span> item.TotalPrice())</span><br><span class="line"></span><br><span class="line"> <span class="keyword">member</span> <span class="keyword">public</span> this.GetTotalPrice () <span class="operator">=</span></span><br><span class="line"> orderItems <span class="operator">|&gt;</span> List.sumBy (<span class="keyword">fun</span> item <span class="operator">-&gt;</span> item.TotalPrice())</span><br></pre></td></tr></table></figure>
diff --git a/2025/03/14/DDD-中的-Ubiquitous-Languages/index.html b/2025/03/14/DDD-中的-Ubiquitous-Languages/index.html
index ff6bf73f..2119c761 100644
--- a/2025/03/14/DDD-中的-Ubiquitous-Languages/index.html
+++ b/2025/03/14/DDD-中的-Ubiquitous-Languages/index.html
@@ -195,17 +195,17 @@
<p>Ubiquitous Language(通用语言)是 Domain-Driven Design (DDD) 中的一个核心概念。它是一种共享的、统一的语言,用于描述和讨论领域模型中的概念和规则。Ubiquitous Language 的目的是确保开发团队和业务专家之间的沟通更加高效和准确,从而减少误解和错误。</p>
<p>一般来说,Ubiquitous Language 有以下特点:</p>
<ol>
-<li><strong>共享</strong>:Ubiquitous Language 是由开发团队和业务专家共同创建和使用的。它不仅用于代码和文档,还用于日常的沟通和讨论。</li>
-<li><strong>统一</strong>:在整个项目中,Ubiquitous Language 应该是一致的。无论是在代码、文档、会议还是白板上,都应该使用相同的术语和概念。</li>
-<li><strong>精确</strong>:Ubiquitous Language 应该尽可能精确,避免模糊和歧义。每个术语都应该有明确的定义和含义。</li>
-<li><strong>演进</strong>:Ubiquitous Language 是动态的,会随着项目的进展和业务需求的变化而演进。团队应该定期审查和更新它。</li>
+<li>共享:Ubiquitous Language 是由开发团队和业务专家共同创建和使用的。它不仅用于代码和文档,还用于日常的沟通和讨论。</li>
+<li>统一:在整个项目中,Ubiquitous Language 应该是一致的。无论是在代码、文档、会议还是白板上,都应该使用相同的术语和概念。</li>
+<li>精确:Ubiquitous Language 应该尽可能精确,避免模糊和歧义。每个术语都应该有明确的定义和含义。</li>
+<li>演进:Ubiquitous Language 是动态的,会随着项目的进展和业务需求的变化而演进。团队应该定期审查和更新它。</li>
</ol>
<p>基于此,它可以用在这些地方:</p>
<ol>
-<li><strong>命名</strong>:在代码中使用 Ubiquitous Language 来命名类、方法、变量等。例如,如果业务专家使用“订单”来描述一个概念,那么在代码中也应该使用“Order”而不是“Purchase”或“Transaction”。</li>
-<li><strong>文档</strong>:在文档中使用 Ubiquitous Language 来描述系统的设计、架构和功能。这有助于业务专家更容易理解技术文档。</li>
-<li><strong>沟通</strong>:在团队会议、讨论和白板会议中使用 Ubiquitous Language。这有助于确保所有参与者都在讨论同一个概念。</li>
-<li><strong>测试</strong>:在编写测试用例时,使用 Ubiquitous Language 来描述测试的前提条件、步骤和预期结果。这有助于确保测试覆盖了业务需求。</li>
+<li>命名:在代码中使用 Ubiquitous Language 来命名类、方法、变量等。例如,如果业务专家使用“订单”来描述一个概念,那么在代码中也应该使用“Order”而不是“Purchase”或“Transaction”。</li>
+<li>文档:在文档中使用 Ubiquitous Language 来描述系统的设计、架构和功能。这有助于业务专家更容易理解技术文档。</li>
+<li>沟通:在团队会议、讨论和白板会议中使用 Ubiquitous Language。这有助于确保所有参与者都在讨论同一个概念。</li>
+<li>测试:在编写测试用例时,使用 Ubiquitous Language 来描述测试的前提条件、步骤和预期结果。这有助于确保测试覆盖了业务需求。</li>
</ol>
<p>假设正在开发一个电子商务系统,业务专家使用“订单”来描述用户购买的商品集合。团队可以在代码中使用“Order”来命名相关的类和方法:</p>
<figure class="highlight fsharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">Order</span> <span class="operator">=</span> &#123;</span><br><span class="line"> items<span class="operator">:</span> OrderItem <span class="type">list</span></span><br><span class="line"> customer<span class="operator">:</span> Customer</span><br><span class="line"> orderDate<span class="operator">:</span> DateTime</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">OrderItem</span> <span class="operator">=</span> &#123;</span><br><span class="line"> product<span class="operator">:</span> Product</span><br><span class="line"> quantity<span class="operator">:</span> <span class="type">int</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Customer</span> <span class="operator">=</span> &#123;</span><br><span class="line"> name<span class="operator">:</span> <span class="type">string</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Product</span> <span class="operator">=</span> &#123;</span><br><span class="line"> name<span class="operator">:</span> <span class="type">string</span></span><br><span class="line"> price<span class="operator">:</span> <span class="type">float</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> addItem (order<span class="operator">:</span> Order) (item<span class="operator">:</span> OrderItem) <span class="operator">=</span></span><br><span class="line"> &#123; order <span class="keyword">with</span> items <span class="operator">=</span> order.items <span class="operator">@</span> [item] &#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> getTotalAmount (order<span class="operator">:</span> Order) <span class="operator">=</span></span><br><span class="line"> <span class="keyword">let</span> total <span class="operator">=</span> <span class="number">0.0</span></span><br><span class="line"> <span class="keyword">for</span> item <span class="keyword">in</span> order.items <span class="keyword">do</span></span><br><span class="line"> total <span class="operator">&lt;-</span> total <span class="operator">+</span> item.price</span><br><span class="line"> total</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> getPrice (item<span class="operator">:</span> OrderItem) <span class="operator">=</span></span><br><span class="line"> item.product.price <span class="operator">*</span> item.quantity</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> getTotalPrice (order<span class="operator">:</span> Order) <span class="operator">=</span></span><br><span class="line"> <span class="keyword">let</span> total <span class="operator">=</span> <span class="number">0.0</span></span><br><span class="line"> <span class="keyword">for</span> item <span class="keyword">in</span> order.items <span class="keyword">do</span></span><br><span class="line"> total <span class="operator">&lt;-</span> total <span class="operator">+</span> getPrice(item)</span><br><span class="line"> total</span><br></pre></td></tr></table></figure>
diff --git a/2025/03/19/领域驱动设计中聚合根持久化和事件发布可能导致数据不一致问题/index.html b/2025/03/19/领域驱动设计中聚合根持久化和事件发布可能导致数据不一致问题/index.html
index f899eeff..de021da0 100644
--- a/2025/03/19/领域驱动设计中聚合根持久化和事件发布可能导致数据不一致问题/index.html
+++ b/2025/03/19/领域驱动设计中聚合根持久化和事件发布可能导致数据不一致问题/index.html
@@ -192,7 +192,7 @@
</div>
</div>
<div class="post-content">
- <p>使用领域事件的一种直接做法是:在 <strong>应用服务</strong> (Application Service) 中产生事件并发布出去。例如,对于“用户昵称更新”的场景来讲,对应的应用服务 <code>UserCommandService</code> 实现如下:</p>
+ <p>使用领域事件的一种直接做法是:在 应用服务 (Application Service) 中产生事件并发布出去。例如,对于“用户昵称更新”的场景来讲,对应的应用服务 <code>UserCommandService</code> 实现如下:</p>
<figure class="highlight fsharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">member</span> <span class="keyword">public</span> this.UpdateMyName (command<span class="operator">:</span> UpdateUsernameCommand) (user<span class="operator">:</span> User) <span class="operator">=</span></span><br><span class="line"> <span class="keyword">let</span> user <span class="operator">=</span> userRepository.GetById user.Id</span><br><span class="line"> <span class="keyword">let</span> oldName <span class="operator">=</span> user.Username</span><br><span class="line"> <span class="keyword">let</span> newName <span class="operator">=</span> command.Username</span><br><span class="line"></span><br><span class="line"> user.UpdateUsername newName</span><br><span class="line"> <span class="operator">|&gt;</span> userRepository.Save</span><br><span class="line"> </span><br><span class="line"> UsernameChangeEvent (user.Id, newName, oldName) </span><br><span class="line"> <span class="operator">|&gt;</span> eventPublisher.Publish</span><br></pre></td></tr></table></figure>
<p>这里,在更新了用户姓名之后,即刻调用事件发布器 <code>eventPublisher.Publish</code> 将事件发送到消息队列中。虽然这种方式比较流行,但它至少存在两个问题:</p>
diff --git a/2025/03/20/TDD-和-DDD-的一些小想法/index.html b/2025/03/20/TDD-和-DDD-的一些小想法/index.html
index e1b89568..a05a3f92 100644
--- a/2025/03/20/TDD-和-DDD-的一些小想法/index.html
+++ b/2025/03/20/TDD-和-DDD-的一些小想法/index.html
@@ -194,27 +194,27 @@
<div class="post-content">
<p>Test-Driven Development (TDD) 和 Domain-Driven Design (DDD) 是两种不同的软件开发方法论,各自有其独特的优缺点和应用场景。</p>
<h3 id="Test-Driven-Development-TDD"><a href="#Test-Driven-Development-TDD" class="headerlink" title="Test-Driven Development (TDD)"></a>Test-Driven Development (TDD)</h3><h4 id="优点:"><a href="#优点:" class="headerlink" title="优点:"></a>优点:</h4><ol>
-<li><strong>提高代码质量</strong>:通过编写测试来驱动开发,可以确保代码的正确性和可靠性。</li>
-<li><strong>减少缺陷</strong>:早期发现和修复缺陷,减少后期的调试和维护成本。</li>
-<li><strong>促进可维护性</strong>:代码更加模块化和可测试,便于后续的维护和扩展。</li>
-<li><strong>文档化</strong>:测试代码本身就是一种文档,描述了系统的行为和期望。</li>
-<li><strong>提高开发速度</strong>:虽然初期可能会慢一些,但长期来看,由于减少了重构和调试的时间,开发速度会提高。</li>
+<li>提高代码质量:通过编写测试来驱动开发,可以确保代码的正确性和可靠性。</li>
+<li>减少缺陷:早期发现和修复缺陷,减少后期的调试和维护成本。</li>
+<li>促进可维护性:代码更加模块化和可测试,便于后续的维护和扩展。</li>
+<li>文档化:测试代码本身就是一种文档,描述了系统的行为和期望。</li>
+<li>提高开发速度:虽然初期可能会慢一些,但长期来看,由于减少了重构和调试的时间,开发速度会提高。</li>
</ol>
<h4 id="缺点:"><a href="#缺点:" class="headerlink" title="缺点:"></a>缺点:</h4><ol>
-<li><strong>初期投入大</strong>:需要在开发前编写测试,初期投入的时间和精力较大。</li>
-<li><strong>学习曲线陡峭</strong>:对新手开发者来说,学习和掌握TDD需要一定的时间。</li>
-<li><strong>过度依赖测试</strong>:可能导致过度依赖单元测试,忽略了系统的整体性能和用户体验。</li>
+<li>初期投入大:需要在开发前编写测试,初期投入的时间和精力较大。</li>
+<li>学习曲线陡峭:对新手开发者来说,学习和掌握TDD需要一定的时间。</li>
+<li>过度依赖测试:可能导致过度依赖单元测试,忽略了系统的整体性能和用户体验。</li>
</ol>
<h3 id="Domain-Driven-Design-DDD"><a href="#Domain-Driven-Design-DDD" class="headerlink" title="Domain-Driven Design (DDD)"></a>Domain-Driven Design (DDD)</h3><h4 id="优点:-1"><a href="#优点:-1" class="headerlink" title="优点:"></a>优点:</h4><ol>
-<li><strong>聚焦领域</strong>:通过深入理解业务领域,确保软件系统与业务需求紧密结合。</li>
-<li><strong>模型驱动</strong>:建立一个清晰的领域模型,帮助开发者和业务专家共同理解系统。</li>
-<li><strong>可维护性</strong>:通过明确的领域模型和边界,系统结构更加清晰,便于维护和扩展。</li>
-<li><strong>沟通桥梁</strong>:提供了一种通用的语言(Ubiquitous Language),促进开发团队和业务团队之间的沟通。</li>
+<li>聚焦领域:通过深入理解业务领域,确保软件系统与业务需求紧密结合。</li>
+<li>模型驱动:建立一个清晰的领域模型,帮助开发者和业务专家共同理解系统。</li>
+<li>可维护性:通过明确的领域模型和边界,系统结构更加清晰,便于维护和扩展。</li>
+<li>沟通桥梁:提供了一种通用的语言(Ubiquitous Language),促进开发团队和业务团队之间的沟通。</li>
</ol>
<h4 id="缺点:-1"><a href="#缺点:-1" class="headerlink" title="缺点:"></a>缺点:</h4><ol>
-<li><strong>复杂性高</strong>:DDD方法论较为复杂,需要深入理解领域模型和设计模式。</li>
-<li><strong>初期投入大</strong>:需要大量的时间和精力进行领域分析和建模。</li>
-<li><strong>适用范围有限</strong>:对于简单的系统或小型项目,DDD可能显得过于复杂和冗余。</li>
+<li>复杂性高:DDD方法论较为复杂,需要深入理解领域模型和设计模式。</li>
+<li>初期投入大:需要大量的时间和精力进行领域分析和建模。</li>
+<li>适用范围有限:对于简单的系统或小型项目,DDD可能显得过于复杂和冗余。</li>
</ol>
<hr>
<p>TDD 和 DDD 并不冲突,实际上它们可以互补。TDD 可以帮助确保代码的正确性和可靠性,而 DDD 可以确保系统与业务需求紧密结合。在进行 TDD 时,可以使用 DDD 的领域模型和 Ubiquitous Language 来编写测试用例,确保测试覆盖了业务需求。在进行 DDD 时,可以使用 TDD 来驱动实现,确保每个领域模型的实现。</p>
diff --git a/2025/03/20/肺功能检查数值/index.html b/2025/03/20/肺功能检查数值/index.html
index fc5c0117..ed5ed1d8 100644
--- a/2025/03/20/肺功能检查数值/index.html
+++ b/2025/03/20/肺功能检查数值/index.html
@@ -194,42 +194,42 @@
<div class="post-content">
<p>肺功能测试是评估患者呼吸系统健康的重要工具。各个指标的及其异常数值可能指示的潜在生理疾病:</p>
<h3 id="1-FEV1(第一秒用力呼气量)"><a href="#1-FEV1(第一秒用力呼气量)" class="headerlink" title="1. FEV1(第一秒用力呼气量)"></a>1. FEV1(第一秒用力呼气量)</h3><ul>
-<li><strong>评估</strong>:FEV1用于评估气道的通畅程度。它反映了在用力呼气的第一秒内,患者能够排出的气体量。</li>
-<li><strong>异常</strong>:FEV1降低通常提示气道阻塞,常见于慢性阻塞性肺病(COPD)、哮喘、支气管炎等疾病。</li>
+<li>评估:FEV1用于评估气道的通畅程度。它反映了在用力呼气的第一秒内,患者能够排出的气体量。</li>
+<li>异常:FEV1降低通常提示气道阻塞,常见于慢性阻塞性肺病(COPD)、哮喘、支气管炎等疾病。</li>
</ul>
<h3 id="2-FVC(用力肺活量)"><a href="#2-FVC(用力肺活量)" class="headerlink" title="2. FVC(用力肺活量)"></a>2. FVC(用力肺活量)</h3><ul>
-<li><strong>评估</strong>:FVC测量的是患者在一次用力呼气中能够排出的最大气体量,反映了肺的容量和扩张能力。</li>
-<li><strong>异常</strong>:FVC降低可能指示限制性肺病,如肺纤维化、胸廓畸形或神经肌肉疾病等。</li>
+<li>评估:FVC测量的是患者在一次用力呼气中能够排出的最大气体量,反映了肺的容量和扩张能力。</li>
+<li>异常:FVC降低可能指示限制性肺病,如肺纤维化、胸廓畸形或神经肌肉疾病等。</li>
</ul>
<h3 id="3-FEV1-FVC比值"><a href="#3-FEV1-FVC比值" class="headerlink" title="3. FEV1&#x2F;FVC比值"></a>3. FEV1&#x2F;FVC比值</h3><ul>
-<li><strong>评估</strong>:FEV1&#x2F;FVC比值用于区分阻塞性和限制性肺病。正常情况下,该比值应大于70%。</li>
-<li><strong>异常</strong>:<ul>
-<li>**低于70%**:提示气道阻塞,常见于COPD和哮喘。</li>
-<li>**正常或高于70%**但FVC降低:可能提示限制性肺病。</li>
+<li>评估:FEV1&#x2F;FVC比值用于区分阻塞性和限制性肺病。正常情况下,该比值应大于70%。</li>
+<li>异常:<ul>
+<li>低于70%:提示气道阻塞,常见于COPD和哮喘。</li>
+<li>正常或高于70%但FVC降低:可能提示限制性肺病。</li>
</ul>
</li>
</ul>
<h3 id="4-PEF(峰值呼气流量)"><a href="#4-PEF(峰值呼气流量)" class="headerlink" title="4. PEF(峰值呼气流量)"></a>4. PEF(峰值呼气流量)</h3><ul>
-<li><strong>评估</strong>:PEF测量患者在用力呼气时达到的最大流速,常用于监测哮喘患者的病情变化。</li>
-<li><strong>异常</strong>:PEF降低可能提示气道狭窄或阻塞,常见于哮喘急性发作或COPD加重。</li>
+<li>评估:PEF测量患者在用力呼气时达到的最大流速,常用于监测哮喘患者的病情变化。</li>
+<li>异常:PEF降低可能提示气道狭窄或阻塞,常见于哮喘急性发作或COPD加重。</li>
</ul>
<h3 id="5-MVV(最大通气量)"><a href="#5-MVV(最大通气量)" class="headerlink" title="5. MVV(最大通气量)"></a>5. MVV(最大通气量)</h3><ul>
-<li><strong>评估</strong>:MVV测量在一定时间内(通常是12秒)能够进行的最大通气量,反映了肺部的通气能力和呼吸肌的力量。</li>
-<li><strong>异常</strong>:MVV降低可能与呼吸肌无力、气道阻塞或肺部疾病(如COPD)相关。</li>
+<li>评估:MVV测量在一定时间内(通常是12秒)能够进行的最大通气量,反映了肺部的通气能力和呼吸肌的力量。</li>
+<li>异常:MVV降低可能与呼吸肌无力、气道阻塞或肺部疾病(如COPD)相关。</li>
</ul>
<h3 id="6-TLC(总肺容量)"><a href="#6-TLC(总肺容量)" class="headerlink" title="6. TLC(总肺容量)"></a>6. TLC(总肺容量)</h3><ul>
-<li><strong>评估</strong>:TLC测量肺部在最大吸气后所能容纳的气体总量,反映了肺的整体容量。</li>
-<li><strong>异常</strong>:<ul>
-<li><strong>增加</strong>:可能与阻塞性肺病(如COPD)相关,因肺部过度膨胀。</li>
-<li><strong>降低</strong>:可能与限制性肺病(如肺纤维化、胸廓畸形)相关。</li>
+<li>评估:TLC测量肺部在最大吸气后所能容纳的气体总量,反映了肺的整体容量。</li>
+<li>异常:<ul>
+<li>增加:可能与阻塞性肺病(如COPD)相关,因肺部过度膨胀。</li>
+<li>降低:可能与限制性肺病(如肺纤维化、胸廓畸形)相关。</li>
</ul>
</li>
</ul>
<h3 id="7-RV(残气量)"><a href="#7-RV(残气量)" class="headerlink" title="7. RV(残气量)"></a>7. RV(残气量)</h3><ul>
-<li><strong>评估</strong>:RV测量在最大呼气后,肺内仍然残留的气体量。</li>
-<li><strong>异常</strong>:<ul>
-<li><strong>增加</strong>:常见于阻塞性肺病,因气道阻塞导致气体无法完全排出。</li>
-<li><strong>降低</strong>:可能与限制性肺病相关。</li>
+<li>评估:RV测量在最大呼气后,肺内仍然残留的气体量。</li>
+<li>异常:<ul>
+<li>增加:常见于阻塞性肺病,因气道阻塞导致气体无法完全排出。</li>
+<li>降低:可能与限制性肺病相关。</li>
</ul>
</li>
</ul>
diff --git a/2025/03/25/Scala-3-Capture-Checking/index.html b/2025/03/25/Scala-3-Capture-Checking/index.html
index c9d6a9a0..08dbf98c 100644
--- a/2025/03/25/Scala-3-Capture-Checking/index.html
+++ b/2025/03/25/Scala-3-Capture-Checking/index.html
@@ -198,19 +198,19 @@
<p>Scala 3 在其类型系统的演进过程中,引入了诸多实验性特性,旨在提升语言的表达能力和代码的可靠性。Capture Checking 是其中一项引人注目的创新。目前还在试验阶段,其通过增强静态分析的能力,在编译时捕获潜在的错误,从而减少运行时问题的发生。</p>
<p>Capture Checking引入了一系列核心概念,这些概念共同构成了其类型系统的基础:</p>
<ul>
-<li><strong>捕获类型 (Capturing Types):</strong> 捕获类型采用 <code>T^&#123;c₁, ..., cᵢ&#125;</code> 的形式,其中 <code>T</code> 是一个普通的 Scala 类型,而 <code>&#123;c₁, ..., cᵢ&#125;</code> 则是一个捕获的 Capabilities 集合。这个捕获集合明确地列出了类型 <code>T</code> 的值所依赖或能够访问的 Capabilities。这种类型表示方式为类型信息增加了一个新的维度,它不仅描述了数据的结构,还包含了数据交互的环境或资源的相关信息。</li>
-<li><strong>Capabilities:</strong> 在 Capture Checking 的语境下,Capabilities 指的是方法或类的参数、局部变量,或者其类型本身就是一个具有非空捕获集合的捕获类型的封闭类的 <code>this</code> 引用。这些实体之所以需要被跟踪,是因为它们通常代表了执行某些操作或访问某些资源的“权限”或“授权”。跟踪这些 Capabilities 意味着可以控制这种影响在程序中的传播方式和范围。</li>
-<li><strong>通用 Capability (<code>cap</code>):</strong> 通用 Capability <code>cap</code> 是一个最基本的 Capability,所有的其他的 Capabilities 都派生自它。类型 <code>T^</code> 是 <code>T^&#123;cap&#125;</code> 的简写形式,表示类型 <code>T</code> 的值可以捕获任意的 Capability。<code>cap</code> 的存在为 Capture Checking 系统提供了一种 <strong>处理精确跟踪并非必需或不可行</strong> 的场景的方式。它充当了一种通配符,表明一个值可能依赖于任何 Capability。</li>
-<li><strong>纯函数 vs. 非纯函数 (Pure vs. Impure Functions):</strong> Capture Checking 显式地区分了纯函数和非纯函数。类型为 <code>A =&gt; B</code> 的函数被认为是非纯函数,它可以捕获任意 Capability,等价于 <code>A -&gt;&#123;cap&#125; B</code> 1。而类型为 <code>A -&gt; B</code> 的函数则是纯函数,它不能捕获任何 Capability。此外,还可以使用 <code>A -&gt;&#123;c, d&#125; B</code> 的形式来显式指定函数只能捕获 Capability <code>c</code> 和 <code>d</code>。这种区分使得类型系统能够强制执行函数式编程的原则,其中纯函数因其可预测性和可测试性而备受推崇。</li>
-<li><strong>子捕获 (Subcapturing):</strong> 子捕获描述了捕获集合之间的关系。如果一个捕获集合 <code>C₁</code> 中的每个元素都包含在另一个捕获集合 <code>C₂</code> 中,那么我们说 <code>C₁</code> 是 <code>C₂</code> 的子捕获,记作 <code>C₁ &lt;: C₂</code> 1。子捕获关系在类型系统中扮演着重要的角色,例如在判断类型兼容性时。它允许具有较小捕获集合的值在需要具有较大捕获集合的地方使用,这基于一种替代原则,即更受限制的 Capability 集合是较少受限制的集合的子类型。</li>
-<li><strong>Capability Classes:</strong> 扩展了 <code>caps.Capability</code> trait 的类被称为 Capability Class。它们的类型捕获集合始终为 <code>&#123;cap&#125;</code>。Capability Class 似乎提供了一种在类型系统中显式定义和管理基本 Capability 的方法。它们与通用 Capability <code>cap</code> 的关联表明,它们具有与环境进行广泛交互的潜力。</li>
+<li>捕获类型 (Capturing Types): 捕获类型采用 <code>T^&#123;c₁, ..., cᵢ&#125;</code> 的形式,其中 <code>T</code> 是一个普通的 Scala 类型,而 <code>&#123;c₁, ..., cᵢ&#125;</code> 则是一个捕获的 Capabilities 集合。这个捕获集合明确地列出了类型 <code>T</code> 的值所依赖或能够访问的 Capabilities。这种类型表示方式为类型信息增加了一个新的维度,它不仅描述了数据的结构,还包含了数据交互的环境或资源的相关信息。</li>
+<li>Capabilities: 在 Capture Checking 的语境下,Capabilities 指的是方法或类的参数、局部变量,或者其类型本身就是一个具有非空捕获集合的捕获类型的封闭类的 <code>this</code> 引用。这些实体之所以需要被跟踪,是因为它们通常代表了执行某些操作或访问某些资源的“权限”或“授权”。跟踪这些 Capabilities 意味着可以控制这种影响在程序中的传播方式和范围。</li>
+<li>通用 Capability (<code>cap</code>): 通用 Capability <code>cap</code> 是一个最基本的 Capability,所有的其他的 Capabilities 都派生自它。类型 <code>T^</code> 是 <code>T^&#123;cap&#125;</code> 的简写形式,表示类型 <code>T</code> 的值可以捕获任意的 Capability。<code>cap</code> 的存在为 Capture Checking 系统提供了一种 <strong>处理精确跟踪并非必需或不可行</strong> 的场景的方式。它充当了一种通配符,表明一个值可能依赖于任何 Capability。</li>
+<li>纯函数 vs. 非纯函数 (Pure vs. Impure Functions): Capture Checking 显式地区分了纯函数和非纯函数。类型为 <code>A =&gt; B</code> 的函数被认为是非纯函数,它可以捕获任意 Capability,等价于 <code>A -&gt;&#123;cap&#125; B</code> 1。而类型为 <code>A -&gt; B</code> 的函数则是纯函数,它不能捕获任何 Capability。此外,还可以使用 <code>A -&gt;&#123;c, d&#125; B</code> 的形式来显式指定函数只能捕获 Capability <code>c</code> 和 <code>d</code>。这种区分使得类型系统能够强制执行函数式编程的原则,其中纯函数因其可预测性和可测试性而备受推崇。</li>
+<li>子捕获 (Subcapturing): 子捕获描述了捕获集合之间的关系。如果一个捕获集合 <code>C₁</code> 中的每个元素都包含在另一个捕获集合 <code>C₂</code> 中,那么我们说 <code>C₁</code> 是 <code>C₂</code> 的子捕获,记作 <code>C₁ &lt;: C₂</code> 1。子捕获关系在类型系统中扮演着重要的角色,例如在判断类型兼容性时。它允许具有较小捕获集合的值在需要具有较大捕获集合的地方使用,这基于一种替代原则,即更受限制的 Capability 集合是较少受限制的集合的子类型。</li>
+<li>Capability Classes: 扩展了 <code>caps.Capability</code> trait 的类被称为 Capability Class。它们的类型捕获集合始终为 <code>&#123;cap&#125;</code>。Capability Class 似乎提供了一种在类型系统中显式定义和管理基本 Capability 的方法。它们与通用 Capability <code>cap</code> 的关联表明,它们具有与环境进行广泛交互的潜力。</li>
</ul>
<blockquote>
<p>Capture Checking 的主要目标是通过静态地跟踪资源或能力的使用情况,从而防止与资源生命周期和可访问性相关的错误。这尤其在涉及资源管理和并发编程的场景中显得至关重要。通过这种静态跟踪,Capture Checking 旨在提高代码的整体安全性,并增强程序的鲁棒性。</p>
</blockquote>
<p>Capture Checking 试图解决编程语言中长期存在的一些问题:</p>
<ul>
-<li><strong>不安全的资源管理:</strong> 例如,传统的 <code>try-with-resources</code> 模式旨在确保资源在使用后被正确关闭。Capture Checking 通过跟踪与资源相关的 Capabilities,可以防止在资源关闭或失效后继续使用它的情况。文档中提到的 <code>usingLogFile</code> 示例就展示了这一点,其中一个闭包尝试写入一个已经关闭的文件,而Capture Checking可以捕获这种不安全的操作:</li>
+<li>不安全的资源管理: 例如,传统的 <code>try-with-resources</code> 模式旨在确保资源在使用后被正确关闭。Capture Checking 通过跟踪与资源相关的 Capabilities,可以防止在资源关闭或失效后继续使用它的情况。文档中提到的 <code>usingLogFile</code> 示例就展示了这一点,其中一个闭包尝试写入一个已经关闭的文件,而Capture Checking可以捕获这种不安全的操作:</li>
</ul>
<figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">usingLogFile</span></span>[<span class="type">T</span>](op: <span class="type">FileOutputStream</span> =&gt; <span class="type">T</span>): <span class="type">T</span> =</span><br><span class="line"> <span class="keyword">val</span> logFile = <span class="type">FileOutputStream</span>(<span class="string">&quot;log&quot;</span>)</span><br><span class="line"> <span class="keyword">val</span> result = op(logFile)</span><br><span class="line"> logFile.close()</span><br><span class="line"> result</span><br></pre></td></tr></table></figure>
@@ -219,47 +219,47 @@
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">| val later = usingLogFile &#123; f =&gt; () =&gt; f.write(0) &#125;</span><br><span class="line">| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span><br><span class="line">|The expression&#x27;s type () =&gt; Unit is not allowed to capture the root capability `cap`.</span><br><span class="line">|This usually means that a capability persists longer than its allowed lifetime.</span><br></pre></td></tr></table></figure>
<ul>
-<li><strong>Effect Polymorphism:</strong> Capture Checking 提供了一种更灵活和精确地推理和控制副作用的机制。它可以被视为一种 Effect system,允许类型系统跟踪和控制代码可能产生的副作用。</li>
-<li><strong>“函数的颜色”问题:</strong> 在异步编程中,区分同步和异步操作一直是一个挑战。Capture Checking 有可能帮助区分和管理同步与异步计算,这通过跟踪与异步操作相关的 Capabilities 来实现。</li>
-<li><strong>基于区域的内存分配:</strong> Capture Checking 可以通过推理与内存位置相关的Capability,来促进更安全的内存管理。这暗示了 Capture Checking 与内存管理之间可能存在更深层次的集成,从而可能实现更高效和更安全的内存使用。</li>
-<li><strong>CE:</strong> Scala 3 通过使用 <code>CanThrow</code> 跟踪抛出特定异常的Capability,实现了一个干净且完全安全的 CE 系统。这提供了一种类型安全的替代方案,相较于传统的 CE,这种方式可能更加灵活和有原则。</li>
-<li><strong>逃逸分析:</strong> Capture Checking 可以防止局部Capability逃逸其预期的作用域。例如,当一个闭包捕获了一个局部Capability,并且这个闭包被赋值给一个全局变量或者以不安全的方式从函数返回时,Capture Checking 可以检测到这种潜在的风险。</li>
+<li>Effect Polymorphism: Capture Checking 提供了一种更灵活和精确地推理和控制副作用的机制。它可以被视为一种 Effect system,允许类型系统跟踪和控制代码可能产生的副作用。</li>
+<li>“函数的颜色”问题: 在异步编程中,区分同步和异步操作一直是一个挑战。Capture Checking 有可能帮助区分和管理同步与异步计算,这通过跟踪与异步操作相关的 Capabilities 来实现。</li>
+<li>基于区域的内存分配: Capture Checking 可以通过推理与内存位置相关的Capability,来促进更安全的内存管理。这暗示了 Capture Checking 与内存管理之间可能存在更深层次的集成,从而可能实现更高效和更安全的内存使用。</li>
+<li>CE: Scala 3 通过使用 <code>CanThrow</code> 跟踪抛出特定异常的Capability,实现了一个干净且完全安全的 CE 系统。这提供了一种类型安全的替代方案,相较于传统的 CE,这种方式可能更加灵活和有原则。</li>
+<li>逃逸分析: Capture Checking 可以防止局部Capability逃逸其预期的作用域。例如,当一个闭包捕获了一个局部Capability,并且这个闭包被赋值给一个全局变量或者以不安全的方式从函数返回时,Capture Checking 可以检测到这种潜在的风险。</li>
</ul>
<hr>
-<p>在 Scala 3 中,函数类型 <code>A =&gt; B</code> 被认为是<strong>不纯的</strong>,它可以捕获任意 Capability。实际上,<code>A =&gt; B</code> 是 <code>A -&gt;&#123;cap&#125; B</code> 的别名,明确地表明了它可能捕获 “通用 Capability”。这种默认行为反映了 Scala 过去函数可以拥有任意副作用的特点。然而,随着 Capture Checking 的引入,开发者被鼓励更明确地表达函数的纯度。</p>
-<p>与不纯函数相对的是<strong>纯函数</strong>,其类型为 <code>A -&gt; B</code>,表示该函数不能捕获任何 Capability。纯函数是函数式编程中的核心概念,Capture Checking 提供了一种在类型层面强制执行纯性的方法。确保函数的纯性可以使代码更具可预测性和可测试性,因为纯函数的输出完全取决于其输入,并且没有副作用。</p>
-<p>开发者还可以<strong>指定函数可以捕获的特定 Capability</strong>,语法为 <code>A -&gt;&#123;c, d&#125; B</code>,表示该函数可以捕获 Capability <code>c</code> 和 <code>d</code>。这种语法允许对函数可以使用的Capability 进行精确控制,从而提高了资源管理的细粒度。通过显式列出捕获的 Capability,编译器可以验证函数是否遵守这些约束,并防止其意外访问其他资源。</p>
-<p><strong>捕获注解</strong> <code>^</code> 的优先级高于 <code>-&gt;</code> 。理解运算符的优先级对于正确解释和编写带有捕获注解的函数类型至关重要。不正确的解析可能导致意想不到的行为或类型错误。例如,<code>A ^ C -&gt; B</code> 表示一个从捕获的 <code>A</code> 到 <code>B</code> 的纯函数。</p>
+<p>在 Scala 3 中,函数类型 <code>A =&gt; B</code> 被认为是不纯的,它可以捕获任意 Capability。实际上,<code>A =&gt; B</code> 是 <code>A -&gt;&#123;cap&#125; B</code> 的别名,明确地表明了它可能捕获 “通用 Capability”。这种默认行为反映了 Scala 过去函数可以拥有任意副作用的特点。然而,随着 Capture Checking 的引入,开发者被鼓励更明确地表达函数的纯度。</p>
+<p>与不纯函数相对的是纯函数,其类型为 <code>A -&gt; B</code>,表示该函数不能捕获任何 Capability。纯函数是函数式编程中的核心概念,Capture Checking 提供了一种在类型层面强制执行纯性的方法。确保函数的纯性可以使代码更具可预测性和可测试性,因为纯函数的输出完全取决于其输入,并且没有副作用。</p>
+<p>开发者还可以指定函数可以捕获的特定 Capability,语法为 <code>A -&gt;&#123;c, d&#125; B</code>,表示该函数可以捕获 Capability <code>c</code> 和 <code>d</code>。这种语法允许对函数可以使用的Capability 进行精确控制,从而提高了资源管理的细粒度。通过显式列出捕获的 Capability,编译器可以验证函数是否遵守这些约束,并防止其意外访问其他资源。</p>
+<p>捕获注解 <code>^</code> 的优先级高于 <code>-&gt;</code> 。理解运算符的优先级对于正确解释和编写带有捕获注解的函数类型至关重要。不正确的解析可能导致意想不到的行为或类型错误。例如,<code>A ^ C -&gt; B</code> 表示一个从捕获的 <code>A</code> 到 <code>B</code> 的纯函数。</p>
<figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">f</span></span>(x: -&gt;&#123;c&#125; <span class="type">Int</span>): <span class="type">Int</span></span><br></pre></td></tr></table></figure>
-<p>Capture Checking 也适用于<strong>上下文函数</strong>。不纯的上下文函数使用 <code>?=&gt;</code>,行为类似于 <code>=&gt;</code>,可以捕获任意Capability。纯的上下文函数使用 <code>?-&gt;</code>,行为类似于 <code>-&gt;</code>,不能捕获任何Capability。这表明,Capture Checking扩展到了上下文函数,允许控制在它们的隐式参数作用域内捕获的 Capability。</p>
-<p>值得注意的是,<strong>方法</strong>本身并不是值,因此它们不直接捕获Capability。相反,它们对 Capability 的引用会被计入封闭对象的捕获集中。这种区分很重要,因为方法的捕获行为与它们所属对象的状态和Capability相关联,这反映了 Scala 的面向对象特性。</p>
-<p>与函数类型类似,Capture Checking的概念也延伸到了<strong>命名参数类型</strong>。<code>=&gt; Int</code> 允许任意Capability引用,类似于不纯函数类型。<code>-&gt; Int</code> 禁止任何Capability引用,类似于纯函数类型。而 <code>-&gt;&#123;c&#125; Int</code> 则只允许引用Capability <code>c</code>。这种一致性确保了即使是延迟求值的表达式也遵循Capability约束。</p>
-<p><strong>子捕获</strong>(<code>C₁ &lt;: C₂</code>)定义了捕获集之间的关系。捕获集 <code>C₁</code> 是 <code>C₂</code> 的子类型,如果 <code>C₂</code> 包含了 <code>C₁</code> 中的每一个元素,并且满足以下条件之一:<code>c ∈ C₂</code>(直接包含);<code>c</code> 是一个类参数,且 <code>C₂</code> 包含 <code>Cls.this</code>(Capability 来源于封闭的类实例);<code>c</code> 的类型具有捕获集 <code>C</code>,且 <code>C &lt;: C₂</code>(基于 Capability 类型的递归子捕获)。子捕获定义了 Capability 依赖的层级结构,这对于类型系统判断一个需要特定 Capability 集合的值是否可以在提供不同 Capability 集合的上下文中使用至关重要。</p>
-<p>对于<strong>捕获类型的子类型</strong>,存在以下规则:纯类型是捕获类型的子类型(<code>T &lt;: C T</code>);较小的捕获集会产生子类型(如果 <code>C₁ &lt;: C₂</code> 且 <code>T₁ &lt;: T₂</code>,则 <code>C₁ T₁ &lt;: C₂ T₂</code>)。这意味着一个依赖较少 Capability 的值通常更通用,可以在更广泛的场景中使用。根 Capability <code>&#123;cap&#125;</code> 覆盖了所有其他捕获集,因此任何特定的捕获集都是 <code>&#123;cap&#125;</code> 的子类型。这允许具有特定捕获要求的类型在允许任何 Capability 的上下文中使用。</p>
-<p><strong>Capability widening</strong>(也称为 _avoidance_)是一种简化局部变量类型的机制。局部变量的类型会被 widening 到不提及该变量本身的最小超类型,这个过程通常会涉及到变量的捕获集。这种加宽有助于改善类型推断和代码清晰度,避免局部变量的类型变得过于复杂。</p>
+<p>Capture Checking 也适用于上下文函数。不纯的上下文函数使用 <code>?=&gt;</code>,行为类似于 <code>=&gt;</code>,可以捕获任意Capability。纯的上下文函数使用 <code>?-&gt;</code>,行为类似于 <code>-&gt;</code>,不能捕获任何Capability。这表明,Capture Checking扩展到了上下文函数,允许控制在它们的隐式参数作用域内捕获的 Capability。</p>
+<p>值得注意的是,方法本身并不是值,因此它们不直接捕获Capability。相反,它们对 Capability 的引用会被计入封闭对象的捕获集中。这种区分很重要,因为方法的捕获行为与它们所属对象的状态和Capability相关联,这反映了 Scala 的面向对象特性。</p>
+<p>与函数类型类似,Capture Checking的概念也延伸到了命名参数类型。<code>=&gt; Int</code> 允许任意Capability引用,类似于不纯函数类型。<code>-&gt; Int</code> 禁止任何Capability引用,类似于纯函数类型。而 <code>-&gt;&#123;c&#125; Int</code> 则只允许引用Capability <code>c</code>。这种一致性确保了即使是延迟求值的表达式也遵循Capability约束。</p>
+<p>子捕获(<code>C₁ &lt;: C₂</code>)定义了捕获集之间的关系。捕获集 <code>C₁</code> 是 <code>C₂</code> 的子类型,如果 <code>C₂</code> 包含了 <code>C₁</code> 中的每一个元素,并且满足以下条件之一:<code>c ∈ C₂</code>(直接包含);<code>c</code> 是一个类参数,且 <code>C₂</code> 包含 <code>Cls.this</code>(Capability 来源于封闭的类实例);<code>c</code> 的类型具有捕获集 <code>C</code>,且 <code>C &lt;: C₂</code>(基于 Capability 类型的递归子捕获)。子捕获定义了 Capability 依赖的层级结构,这对于类型系统判断一个需要特定 Capability 集合的值是否可以在提供不同 Capability 集合的上下文中使用至关重要。</p>
+<p>对于捕获类型的子类型,存在以下规则:纯类型是捕获类型的子类型(<code>T &lt;: C T</code>);较小的捕获集会产生子类型(如果 <code>C₁ &lt;: C₂</code> 且 <code>T₁ &lt;: T₂</code>,则 <code>C₁ T₁ &lt;: C₂ T₂</code>)。这意味着一个依赖较少 Capability 的值通常更通用,可以在更广泛的场景中使用。根 Capability <code>&#123;cap&#125;</code> 覆盖了所有其他捕获集,因此任何特定的捕获集都是 <code>&#123;cap&#125;</code> 的子类型。这允许具有特定捕获要求的类型在允许任何 Capability 的上下文中使用。</p>
+<p>Capability widening(也称为 _avoidance_)是一种简化局部变量类型的机制。局部变量的类型会被 widening 到不提及该变量本身的最小超类型,这个过程通常会涉及到变量的捕获集。这种加宽有助于改善类型推断和代码清晰度,避免局部变量的类型变得过于复杂。</p>
<figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">fs: <span class="type">FileSystem</span>^</span><br><span class="line">ct: <span class="type">CanThrow</span>[<span class="type">Exception</span>]^</span><br><span class="line">l : <span class="type">Logger</span>^&#123;fs&#125;</span><br><span class="line"></span><br><span class="line">&#123;l&#125; &lt;: &#123;fs&#125; &lt;: &#123;cap&#125;</span><br><span class="line">&#123;fs&#125; &lt;: &#123;fs, ct&#125; &lt;: &#123;cap&#125;</span><br><span class="line">&#123;ct&#125; &lt;: &#123;fs, ct&#125; &lt;: &#123;cap&#125;</span><br></pre></td></tr></table></figure>
<p>继承自 <code>caps.Capability</code> 的类具有隐式的 <code>&#123;cap&#125;</code> 捕获集。这表明这些类的实例本质上代表了一种 Capability。Capability 类提供了一种将 Capability 显式定义和管理为类型系统中的一等公民的方式。开发者可以创建具有特定语义和使用模式的自定义 Capability。</p>
-<p>在使用 Capability 类的场景中,通常会结合 using clauses 和<strong>隐式参数</strong>来减少在代码中显式传递 Capability 的需要。这两种方法提供了一种自动将必要的 Capability 提供给函数和方法的方式,从而减少了样板代码并提高了代码的可读性。</p>
-<p><strong>闭包</strong>会捕获在其主体中引用的来自其周围环境的 Capability。这导致闭包的函数类型中包含捕获集。例如,如果一个闭包引用了一个局部变量 <code>fs</code>,而 <code>fs</code> 是一个 Capability,那么该闭包的类型可能就是 <code>String -&gt;&#123;fs&#125; Unit</code>。这意味着闭包继承了其封闭代码的 Capability 要求,确保它们只能在这些 Capability 可用的上下文中被使用。</p>
+<p>在使用 Capability 类的场景中,通常会结合 using clauses 和隐式参数来减少在代码中显式传递 Capability 的需要。这两种方法提供了一种自动将必要的 Capability 提供给函数和方法的方式,从而减少了样板代码并提高了代码的可读性。</p>
+<p>闭包会捕获在其主体中引用的来自其周围环境的 Capability。这导致闭包的函数类型中包含捕获集。例如,如果一个闭包引用了一个局部变量 <code>fs</code>,而 <code>fs</code> 是一个 Capability,那么该闭包的类型可能就是 <code>String -&gt;&#123;fs&#125; Unit</code>。这意味着闭包继承了其封闭代码的 Capability 要求,确保它们只能在这些 Capability 可用的上下文中被使用。</p>
<p>此外,闭包还会捕获它们调用的函数的 Capability。如果一个闭包调用了一个需要特定 Capability 的函数,那么该闭包的捕获集也会包含该 Capability。这种传递性的捕获机制确保了所有 Capability 依赖,无论是直接的还是间接的,都会被闭包所跟踪。</p>
<p>类会将其方法中使用的 Capability 保留为(私有)字段。这意味着如果一个方法使用了作为构造函数参数传递的Capability,该类很可能会存储对它的引用。这会导致类的类型中包含捕获集。例如,一个使用文件系统Capability <code>xfs</code> 的 <code>Logger</code> 类可能具有类型 <code>Logger^&#123;xfs&#125;</code>。这表明类封装了它们所依赖的Capability,并将这些依赖作为其类型签名的一部分。</p>
<p><code>@constructorOnly</code> 注解可以用于标记一个仅在构造函数中使用而不会作为字段保留的类参数。这有助于减少类的捕获集。如果一个参数仅用于初始化,后续不再访问,那么类就没有必要将其保留为一种 Capability。</p>
-<p>类的<strong>捕获引用</strong>包括从类外部使用的局部 Capability 以及具有捕获类型的构造函数参数(参数Capability)。局部Capability会被内部类继承。</p>
+<p>类的捕获引用包括从类外部使用的局部 Capability 以及具有捕获类型的构造函数参数(参数Capability)。局部Capability会被内部类继承。</p>
<p>类实例的 <code>this</code> 的捕获集是根据捕获的引用、父类以及类内部的使用约束来推断的 1。这种自动推断机制在很多情况下减少了手动指定类捕获集的需要。</p>
<figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> caps.<span class="type">Capability</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">FileSystem</span> <span class="keyword">extends</span> <span class="title">Capability</span></span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Logger</span>(<span class="params">using <span class="type">FileSystem</span></span>)</span>:</span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">log</span></span>(s: <span class="type">String</span>): <span class="type">Unit</span> = ???</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">test</span></span>(<span class="keyword">using</span> fs: <span class="type">FileSystem</span>) =</span><br><span class="line"> <span class="keyword">val</span> l: <span class="type">Logger</span>^&#123;fs&#125; = <span class="type">Logger</span>()</span><br><span class="line"> ...</span><br></pre></td></tr></table></figure>
<figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Logger</span>(<span class="params">using <span class="type">FileSystem</span>^&#123;cap&#125;</span>)</span>:</span><br><span class="line"> ^^^^^^^^^^^^^^</span><br><span class="line"> redundant capture: <span class="type">FileSystem</span> already accounts <span class="keyword">for</span> cap</span><br></pre></td></tr></table></figure>
-<p><strong>捕获隧道 (Capture Tunnelling)</strong> 是指当一个类型变量被一个捕获类型实例化时,捕获信息不会立即传播到外层的泛型类型。相反,捕获会“穿过隧道”,并在类型变量被访问或其成员被使用时重新出现。这种机制有助于以更简洁和可管理的方式处理泛型代码中的捕获集,避免类型签名过于复杂。</p>
-<p><strong>逃逸检查</strong>施加了一些限制。作为类型变量实例的捕获类型不能携带通用Capability <code>cap</code> 。可变变量也不能拥有通用捕获集 。逃逸检查阻止了在参数化类型的参数中返回或分配带有局部 Capability 的闭包,因为这可能导致 Capability 逃逸其预期的作用域。<strong>单调性规则</strong>指出,在一个带有字段 <code>f</code> 的类中,<code>&#123;this&#125;</code> 覆盖了 <code>&#123;this.f&#125;</code> 以及 <code>this.f</code> 对纯参数的应用。这意味着如果类实例本身被视为一种 Capability,那么其字段所持有的任何 Capability 也会被隐式地覆盖。逃逸检查对于维护 Capability 跟踪的完整性至关重要,它可以防止 Capability 在其预期生命周期或作用域之外被使用,尤其是在泛型和可变状态的上下文中。</p>
-<p><strong>受检异常</strong>可以通过导入 <code>language.experimental.saferExceptions</code> 来启用。方法上的 <code>throws</code> 子句会扩展为一个隐式的 <code>CanThrow</code> Capability参数,表明该方法可能抛出指定类型的异常。<code>throw</code> 表达式需要 <code>CanThrow</code> Capability,而 <code>try</code> 表达式会创建这种Capability。在 <code>language.experimental.captureChecking</code> 下,由于逃逸的 Capability 而导致未处理异常的代码会被拒绝。为了实现这种集成,<code>CanThrow</code> 需要继承 <code>Capability</code>,并且需要将逃逸检查扩展到 <code>try</code> 表达式,以防止捕获 <code>cap</code>。Capture Checking 与受检异常的集成确保了异常的可能性也被作为一种 Capability 需求来跟踪,从而加强了语言的整体资源管理和错误处理 Capability。</p>
+<p>捕获隧道 (Capture Tunnelling) 是指当一个类型变量被一个捕获类型实例化时,捕获信息不会立即传播到外层的泛型类型。相反,捕获会“穿过隧道”,并在类型变量被访问或其成员被使用时重新出现。这种机制有助于以更简洁和可管理的方式处理泛型代码中的捕获集,避免类型签名过于复杂。</p>
+<p>逃逸检查施加了一些限制。作为类型变量实例的捕获类型不能携带通用Capability <code>cap</code> 。可变变量也不能拥有通用捕获集 。逃逸检查阻止了在参数化类型的参数中返回或分配带有局部 Capability 的闭包,因为这可能导致 Capability 逃逸其预期的作用域。单调性规则指出,在一个带有字段 <code>f</code> 的类中,<code>&#123;this&#125;</code> 覆盖了 <code>&#123;this.f&#125;</code> 以及 <code>this.f</code> 对纯参数的应用。这意味着如果类实例本身被视为一种 Capability,那么其字段所持有的任何 Capability 也会被隐式地覆盖。逃逸检查对于维护 Capability 跟踪的完整性至关重要,它可以防止 Capability 在其预期生命周期或作用域之外被使用,尤其是在泛型和可变状态的上下文中。</p>
+<p>受检异常可以通过导入 <code>language.experimental.saferExceptions</code> 来启用。方法上的 <code>throws</code> 子句会扩展为一个隐式的 <code>CanThrow</code> Capability参数,表明该方法可能抛出指定类型的异常。<code>throw</code> 表达式需要 <code>CanThrow</code> Capability,而 <code>try</code> 表达式会创建这种Capability。在 <code>language.experimental.captureChecking</code> 下,由于逃逸的 Capability 而导致未处理异常的代码会被拒绝。为了实现这种集成,<code>CanThrow</code> 需要继承 <code>Capability</code>,并且需要将逃逸检查扩展到 <code>try</code> 表达式,以防止捕获 <code>cap</code>。Capture Checking 与受检异常的集成确保了异常的可能性也被作为一种 Capability 需求来跟踪,从而加强了语言的整体资源管理和错误处理 Capability。</p>
<hr>
-<p>文档中提供了一个关于<strong>惰性列表</strong>(Lazy Lists)的较大示例,很好地展示了 Capture Checking 如何在更复杂的数据结构中工作。<code>LzyList</code> 的 <code>tail</code> 具有一个捕获注解,表明它可以捕获与列表相同的引用。诸如 <code>map</code>、<code>filter</code> 和 <code>concat</code> 等操作在惰性列表上能够正确地推断出捕获集,这取决于原始列表和所使用的函数。值得注意的是 effect polymorphism的概念,传递给这些操作的纯函数不会出现在结果的捕获集中。这与严格列表形成对比,严格列表通常不需要捕获注解,因为它们的副作用不会被延迟。惰性列表的例子具体说明了Capture Checking 如何应用于非平凡的数据结构,展示了其在涉及延迟求值的复杂场景中跟踪依赖关系的Capability。effect polymorphism 是一个显著的优点,它允许在不影响捕获集的情况下使用纯计算。</p>
+<p>文档中提供了一个关于惰性列表(Lazy Lists)的较大示例,很好地展示了 Capture Checking 如何在更复杂的数据结构中工作。<code>LzyList</code> 的 <code>tail</code> 具有一个捕获注解,表明它可以捕获与列表相同的引用。诸如 <code>map</code>、<code>filter</code> 和 <code>concat</code> 等操作在惰性列表上能够正确地推断出捕获集,这取决于原始列表和所使用的函数。值得注意的是 effect polymorphism的概念,传递给这些操作的纯函数不会出现在结果的捕获集中。这与严格列表形成对比,严格列表通常不需要捕获注解,因为它们的副作用不会被延迟。惰性列表的例子具体说明了Capture Checking 如何应用于非平凡的数据结构,展示了其在涉及延迟求值的复杂场景中跟踪依赖关系的Capability。effect polymorphism 是一个显著的优点,它允许在不影响捕获集的情况下使用纯计算。</p>
<hr>
-<p><strong>表 1: 代码示例与演示概念</strong></p>
+<p>表 1: 代码示例与演示概念</p>
<table>
<thead>
<tr>
@@ -269,9 +269,9 @@
</tr>
</thead>
<tbody><tr>
-<td><strong>代码片段</strong></td>
-<td><strong>演示概念</strong></td>
-<td><strong>捕获行为解释</strong></td>
+<td>代码片段</td>
+<td>演示概念</td>
+<td>捕获行为解释</td>
</tr>
<tr>
<td><code>import language.experimental.captureChecking</code></td>
@@ -340,11 +340,11 @@
</tr>
</tbody></table>
<p>当 <code>cap</code> 出现在函数的结果类型中时,通常表示一个由存在性量词绑定的未知类型(例如,<code>() -&gt; Iterator^</code> 意味着 <code>() -&gt; Exists x. Iterator^x</code>)。这表明返回的迭代器可能捕获了某种 Capability,但具体的哪种 Capability 在静态类型检查时是未知的。在内部,这种存在性 Capability 使用带有 sealed trait <code>Exists</code> 的依赖函数类型来表示 。结果类型中协变的 <code>cap</code> 会被替换为一个新的 existential variable。当应用一个具有 existential result 类型 <code>Exists ex.T</code> 的函数时,结果是 <code>T</code>,其中 <code>ex</code> 被 <code>cap</code> 替换。Existential Capability 允许类型系统表达在编译时具体捕获的 Capability 未知的情况,从而提供了灵活性,同时仍然保持了一定程度的跟踪。</p>
-<p>**Reach Capability **用于表达一个变量引用了通过另一个Capability“Reach”的任何操作。例如,如果 <code>ops</code> 是一个表示一组操作的Capability,那么 <code>ops*</code> 就表示出现在 <code>ops</code> 类型中且通过 <code>ops</code> 访问的任何协变 Capability。Reach Capability 提供了一种间接推理和跟踪 Capability 的方式,这对于建模具有相互连接资源的复杂系统非常有用。</p>
-<p><strong>Capability 多态</strong>允许使用带有上界 <code>CapSet</code> 的类型变量来参数化操作的捕获集。这使得定义诸如 <code>Source[X^]</code> 这样的类型成为可能,其中 <code>X^</code> 表示监听器可以持有的一组 Capability。Capability 多态增强了代码的表达性和可重用性,因为它允许函数和数据结构在它们可能依赖的 Capability 集上进行参数化。</p>
+<p>Reach Capability 用于表达一个变量引用了通过另一个Capability“Reach”的任何操作。例如,如果 <code>ops</code> 是一个表示一组操作的Capability,那么 <code>ops*</code> 就表示出现在 <code>ops</code> 类型中且通过 <code>ops</code> 访问的任何协变 Capability。Reach Capability 提供了一种间接推理和跟踪 Capability 的方式,这对于建模具有相互连接资源的复杂系统非常有用。</p>
+<p>Capability 多态允许使用带有上界 <code>CapSet</code> 的类型变量来参数化操作的捕获集。这使得定义诸如 <code>Source[X^]</code> 这样的类型成为可能,其中 <code>X^</code> 表示监听器可以持有的一组 Capability。Capability 多态增强了代码的表达性和可重用性,因为它允许函数和数据结构在它们可能依赖的 Capability 集上进行参数化。</p>
<hr>
<p>Scala 3 提供了几个与 Capture Checking 相关的编译选项,。<code>-Xprint:cc</code> 选项会打印出带有推断捕获类型的程序代码 1。这对于理解编译器是如何推断捕获集的以及调试与类型相关的错误非常有用。另一个选项是 <code>-Ycc-debug</code>,它提供了关于 Capture Checking 过程的详细的、面向实现的的信息。这个选项对于需要深入了解 Capture Checking 器工作原理或者遇到复杂问题的开发者来说很有帮助。这些编译选项是使用 Capture Checking 的开发者的重要工具,它们允许开发者检查编译器的推理并诊断问题。</p>
-<p>Capture Checking被实现为一个传播约束求解器,它在标准的类型检查阶段之后运行,未知的捕获集由约束变量表示。在类型中显式编写的捕获集被求解器视为常量。类型之间的子类型要求会转化为它们各自捕获集上的子捕获测试。求解器根据程序的结构和类型约束,将 Capability 传播给约束变量及其超集。捕获集的映射受到类型参数的变性(协变、逆变、不变)的影响。<strong>装箱</strong>(boxing)和<strong>拆箱</strong>(unboxing)是用于隐藏和恢复捕获集的虚拟操作,特别是在类型参数的上下文中用于管理捕获隧道。<code>-Ycc-debug</code> 的输出提供了关于变量依赖关系和Capture Checking器在编译过程中的状态的深入信息。</p>
+<p>Capture Checking被实现为一个传播约束求解器,它在标准的类型检查阶段之后运行,未知的捕获集由约束变量表示。在类型中显式编写的捕获集被求解器视为常量。类型之间的子类型要求会转化为它们各自捕获集上的子捕获测试。求解器根据程序的结构和类型约束,将 Capability 传播给约束变量及其超集。捕获集的映射受到类型参数的变性(协变、逆变、不变)的影响。装箱(boxing)和拆箱(unboxing)是用于隐藏和恢复捕获集的虚拟操作,特别是在类型参数的上下文中用于管理捕获隧道。<code>-Ycc-debug</code> 的输出提供了关于变量依赖关系和Capture Checking器在编译过程中的状态的深入信息。</p>
</div>
diff --git a/2025/03/31/在F-中处理复杂依赖注入的实践指南/index.html b/2025/03/31/在F-中处理复杂依赖注入的实践指南/index.html
index 49034c9f..090d8078 100644
--- a/2025/03/31/在F-中处理复杂依赖注入的实践指南/index.html
+++ b/2025/03/31/在F-中处理复杂依赖注入的实践指南/index.html
@@ -195,12 +195,12 @@
<h2 id="一、传统手艺:Partial-Application"><a href="#一、传统手艺:Partial-Application" class="headerlink" title="一、传统手艺:Partial Application"></a>一、传统手艺:Partial Application</h2><p>在函数式编程中,Partial Application 是传递依赖的常用方式。例如:</p>
<figure class="highlight fsharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> foo bar baz request <span class="operator">=</span> <span class="operator">...</span></span><br><span class="line"><span class="keyword">let</span> wired <span class="operator">=</span> foo dependency1 dependency2</span><br><span class="line"><span class="keyword">let</span> response <span class="operator">=</span> wired request</span><br></pre></td></tr></table></figure>
-<p><strong>优点</strong>: </p>
+<p>优点: </p>
<ul>
<li>无需框架或反射,直接通过函数参数传递依赖。 </li>
<li>符合函数式编程的纯函数理念。</li>
</ul>
-<p><strong>缺点</strong>: </p>
+<p>缺点: </p>
<ul>
<li>参数爆炸:当功能扩展时,参数数量激增(如日志、数据库、加密等)。 </li>
<li>维护困难:新增依赖需修改所有调用点的参数传递。 </li>
@@ -210,13 +210,13 @@
<h2 id="二、结构化方法:单一环境参数(env)"><a href="#二、结构化方法:单一环境参数(env)" class="headerlink" title="二、结构化方法:单一环境参数(env)"></a>二、结构化方法:单一环境参数(<code>env</code>)</h2><p>为解决参数爆炸问题,可将依赖封装为单一环境对象<code>env</code>,并通过接口约束访问权限:</p>
<figure class="highlight fsharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">[&lt;Interface&gt;]</span> <span class="keyword">type</span> <span class="title class_">ILog</span> <span class="operator">=</span> <span class="keyword">abstract</span> Logger<span class="operator">:</span> ILogger</span><br><span class="line"><span class="meta">[&lt;Interface&gt;]</span> <span class="keyword">type</span> <span class="title class_">IDb</span> <span class="operator">=</span> <span class="keyword">abstract</span> Database<span class="operator">:</span> IDatabase</span><br><span class="line"></span><br><span class="line"><span class="keyword">module</span> Log <span class="operator">=</span></span><br><span class="line"> <span class="keyword">let</span> info (env<span class="operator">:</span> #ILog) <span class="operator">=</span> env.Logger.Info(<span class="string">&quot;Message&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">module</span> Db <span class="operator">=</span></span><br><span class="line"> <span class="keyword">let</span> fetchUser (env<span class="operator">:</span> #IDb) <span class="operator">=</span> env.Database.Query(<span class="operator">...</span>)</span><br></pre></td></tr></table></figure>
-<p><strong>优点</strong>: </p>
+<p>优点: </p>
<ul>
-<li><strong>显式依赖声明</strong>:函数签名仅需<code>env</code>参数,编译器验证接口实现。 </li>
-<li><strong>模块化隔离</strong>:各模块仅声明所需接口(如<code>ILog</code>、<code>IDb</code>),避免全局依赖。 </li>
-<li><strong>易于测试</strong>:通过模拟<code>env</code>实现单元测试,无需依赖具体实现。</li>
+<li>显式依赖声明:函数签名仅需<code>env</code>参数,编译器验证接口实现。 </li>
+<li>模块化隔离:各模块仅声明所需接口(如<code>ILog</code>、<code>IDb</code>),避免全局依赖。 </li>
+<li>易于测试:通过模拟<code>env</code>实现单元测试,无需依赖具体实现。</li>
</ul>
-<p><strong>应用场景</strong>: </p>
+<p>应用场景: </p>
<figure class="highlight fsharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> changePass env req <span class="operator">=</span> <span class="keyword">task</span> &#123;</span><br><span class="line"> <span class="keyword">let!</span> user <span class="operator">=</span> Db.fetchUser env req.UserId</span><br><span class="line"> Log.info env <span class="string">&quot;Processing user: %i&quot;</span> user.Id</span><br><span class="line"> <span class="operator">...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<hr>
@@ -226,15 +226,15 @@
<p>然后:</p>
<figure class="highlight fsharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> changePass req <span class="operator">=</span> <span class="keyword">effect</span> &#123;</span><br><span class="line"> <span class="keyword">let!</span> user <span class="operator">=</span> Db.fetchUser req.UserId</span><br><span class="line"> <span class="keyword">let!</span> salt <span class="operator">=</span> Random.bytes <span class="number">32</span></span><br><span class="line"> <span class="keyword">do!</span> Log.info <span class="string">&quot;Password updated for user %i&quot;</span> user.Id</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">Ok</span>()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
-<p><strong>优点</strong>: </p>
+<p>优点: </p>
<ul>
-<li><strong>隐式依赖管理</strong>:通过<code>effect</code>计算表达式自动传递<code>env</code>,减少样板代码。 </li>
-<li><strong>组合性</strong>:支持与其他计算表达式(如<code>async</code>&#x2F;<code>task</code>)结合,处理异步操作。</li>
+<li>隐式依赖管理:通过<code>effect</code>计算表达式自动传递<code>env</code>,减少样板代码。 </li>
+<li>组合性:支持与其他计算表达式(如<code>async</code>&#x2F;<code>task</code>)结合,处理异步操作。</li>
</ul>
-<p><strong>缺点</strong>: </p>
+<p>缺点: </p>
<ul>
-<li><strong>性能开销</strong>:频繁的闭包创建和间接调用可能导致性能下降。 </li>
-<li><strong>生态兼容性</strong>:需自定义计算表达式,与现有异步框架集成复杂。</li>
+<li>性能开销:频繁的闭包创建和间接调用可能导致性能下降。 </li>
+<li>生态兼容性:需自定义计算表达式,与现有异步框架集成复杂。</li>
</ul>
<h2 id="Refs"><a href="#Refs" class="headerlink" title="Refs."></a>Refs.</h2><ul>
<li>Spring的构造器注入</li>
diff --git a/2025/04/02/N-1-selects-problem-与-Prisma-ORM/index.html b/2025/04/02/N-1-selects-problem-与-Prisma-ORM/index.html
index c12c8c1f..6a5cd15f 100644
--- a/2025/04/02/N-1-selects-problem-与-Prisma-ORM/index.html
+++ b/2025/04/02/N-1-selects-problem-与-Prisma-ORM/index.html
@@ -192,31 +192,31 @@
</div>
</div>
<div class="post-content">
- <p>N+1 查询问题是指在通过 ORM 查询数据时,执行了<strong>一次</strong>初始查询来获取父对象列表(这 <strong>1</strong> 次查询),然后为列表中的<strong>每一个</strong>父对象都单独执行了一次额外的查询来获取其关联的子对象(这 <strong>N</strong> 次查询)。最终导致总共执行了 <strong>1 + N</strong> 次数据库查询,其中 N 是初始查询返回的父对象的数量。</p>
-<p><strong>举个例子:</strong></p>
+ <p>N+1 查询问题是指在通过 ORM 查询数据时,执行了一次初始查询来获取父对象列表(这 1 次查询),然后为列表中的每一个父对象都单独执行了一次额外的查询来获取其关联的子对象(这 N 次查询)。最终导致总共执行了 1 + N 次数据库查询,其中 N 是初始查询返回的父对象的数量。</p>
+<p>举个例子:</p>
<p>假设有两个数据库模型:<code>User</code>(用户)和 <code>Post</code>(帖子),一个用户可以有多篇帖子(一对多关系)。</p>
<p>现在,需要获取前 10 个用户以及他们各自的所有帖子。</p>
-<p>一种<strong>有问题</strong>的 ORM 实现(或不当的使用方式)可能会这样执行:</p>
+<p>一种有问题的 ORM 实现(或不当的使用方式)可能会这样执行:</p>
<ol>
-<li><strong>第一次查询 (The “1”)</strong>: 获取前 10 个用户。<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">User</span> LIMIT <span class="number">10</span>;</span><br></pre></td></tr></table></figure></li>
-<li><strong>接下来的 N (&#x3D;10) 次查询 (The “N”)</strong>: 对于上一步获取到的每一个用户,单独执行一次查询来获取该用户的帖子。<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 用户 1</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> Post <span class="keyword">WHERE</span> authorId <span class="operator">=</span> <span class="number">1</span>;</span><br><span class="line"><span class="comment">-- 用户 2</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> Post <span class="keyword">WHERE</span> authorId <span class="operator">=</span> <span class="number">2</span>;</span><br><span class="line"><span class="comment">-- 用户 3</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> Post <span class="keyword">WHERE</span> authorId <span class="operator">=</span> <span class="number">3</span>;</span><br><span class="line"><span class="comment">-- ... 直到 用户 10</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> Post <span class="keyword">WHERE</span> authorId <span class="operator">=</span> <span class="number">10</span>;</span><br></pre></td></tr></table></figure></li>
+<li>第一次查询 (The “1”): 获取前 10 个用户。<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">User</span> LIMIT <span class="number">10</span>;</span><br></pre></td></tr></table></figure></li>
+<li>接下来的 N (&#x3D;10) 次查询 (The “N”): 对于上一步获取到的每一个用户,单独执行一次查询来获取该用户的帖子。<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 用户 1</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> Post <span class="keyword">WHERE</span> authorId <span class="operator">=</span> <span class="number">1</span>;</span><br><span class="line"><span class="comment">-- 用户 2</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> Post <span class="keyword">WHERE</span> authorId <span class="operator">=</span> <span class="number">2</span>;</span><br><span class="line"><span class="comment">-- 用户 3</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> Post <span class="keyword">WHERE</span> authorId <span class="operator">=</span> <span class="number">3</span>;</span><br><span class="line"><span class="comment">-- ... 直到 用户 10</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> Post <span class="keyword">WHERE</span> authorId <span class="operator">=</span> <span class="number">10</span>;</span><br></pre></td></tr></table></figure></li>
</ol>
-<p>在这个场景下,总共执行了 1 + 10 &#x3D; <strong>11</strong> 次数据库查询。如果 N 的值很大(比如获取 1000 个用户),就会产生 1001 次查询,这对数据库造成巨大的、不必要的压力,并显著增加应用程序的响应时间。每一次数据库交互都有网络延迟和数据库处理的开销,N+1 次查询会将这些开销放大 N 倍。</p>
+<p>在这个场景下,总共执行了 1 + 10 &#x3D; 11 次数据库查询。如果 N 的值很大(比如获取 1000 个用户),就会产生 1001 次查询,这对数据库造成巨大的、不必要的压力,并显著增加应用程序的响应时间。每一次数据库交互都有网络延迟和数据库处理的开销,N+1 次查询会将这些开销放大 N 倍。</p>
<p>N+1 问题通常源于 ORM 处理关联数据的方式,特别是与“懒加载”(Lazy Loading)相关的策略。懒加载是指只有在显式访问关联属性时,ORM 才会去数据库加载这些数据。虽然这在某些情况下可以避免加载不需要的数据,但如果在循环中访问关联属性,就很容易触发 N+1 问题。</p>
-<p>然而,问题的根源在于<strong>没有有效地预先加载(或批量加载)所需的关联数据</strong>。即使不使用严格意义上的懒加载,如果 ORM 在处理关联查询时不够智能,采用了逐个获取关联对象的策略,同样会产生 N+1 查询。</p>
+<p>然而,问题的根源在于没有有效地预先加载(或批量加载)所需的关联数据。即使不使用严格意义上的懒加载,如果 ORM 在处理关联查询时不够智能,采用了逐个获取关联对象的策略,同样会产生 N+1 查询。</p>
<p>在 Prisma 出现之前或在其他 ORM 中,解决 N+1 问题常见的方法包括:</p>
<ol>
-<li><strong>预先加载(Eager Loading)</strong>: 在执行初始查询时,就明确指示 ORM 同时将关联数据也查询出来。这通常通过 SQL 的 <code>JOIN</code> 操作实现。例如,一次性查询出用户和他们的帖子。虽然这减少了查询次数,但复杂的 <code>JOIN</code> 可能会导致查询本身变得庞大和低效,并可能返回冗余数据。</li>
-<li><strong>批量加载(Batch Loading)</strong>: 先执行初始查询获取父对象列表,然后收集所有父对象的 ID,在第二次查询中使用 <code>WHERE IN (...)</code> 子句一次性加载所有相关的子对象。这种方式通常需要两次查询,但避免了 N 次单独的查询。</li>
+<li>预先加载(Eager Loading): 在执行初始查询时,就明确指示 ORM 同时将关联数据也查询出来。这通常通过 SQL 的 <code>JOIN</code> 操作实现。例如,一次性查询出用户和他们的帖子。虽然这减少了查询次数,但复杂的 <code>JOIN</code> 可能会导致查询本身变得庞大和低效,并可能返回冗余数据。</li>
+<li>批量加载(Batch Loading): 先执行初始查询获取父对象列表,然后收集所有父对象的 ID,在第二次查询中使用 <code>WHERE IN (...)</code> 子句一次性加载所有相关的子对象。这种方式通常需要两次查询,但避免了 N 次单独的查询。</li>
</ol>
-<p>Prisma ORM 在设计上就考虑了 N+1 问题,并提供了一种既方便开发者又高效的解决方案。当使用 Prisma Client 查询数据并需要包含关联模型时,Prisma 会自动优化查询,<strong>避免产生 N+1 查询</strong>,主要通过<strong>关系查询(Relation Queries)</strong>中的 <code>include</code> 选项或嵌套读取(nested reads)来实现这一点:</p>
+<p>Prisma ORM 在设计上就考虑了 N+1 问题,并提供了一种既方便开发者又高效的解决方案。当使用 Prisma Client 查询数据并需要包含关联模型时,Prisma 会自动优化查询,避免产生 N+1 查询,主要通过关系查询(Relation Queries)中的 <code>include</code> 选项或嵌套读取(nested reads)来实现这一点:</p>
<p>假设想获取所有用户及其发布的帖子,使用 Prisma Client,可以这样写:</p>
<figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">PrismaClient</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@prisma/client&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> prisma = <span class="keyword">new</span> <span class="title class_">PrismaClient</span>()</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">getUsersWithPosts</span>(<span class="params"></span>) &#123;</span><br><span class="line"> <span class="keyword">const</span> usersWithPosts = <span class="keyword">await</span> prisma.<span class="property">user</span>.<span class="title function_">findMany</span>(&#123;</span><br><span class="line"> <span class="attr">include</span>: &#123;</span><br><span class="line"> <span class="attr">posts</span>: <span class="literal">true</span>, <span class="comment">// 指示 Prisma 加载关联的 posts</span></span><br><span class="line"> &#125;,</span><br><span class="line"> &#125;)</span><br><span class="line"> <span class="comment">// usersWithPosts 包含了用户列表,每个用户对象中都有一个 posts 数组</span></span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(usersWithPosts)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="title function_">getUsersWithPosts</span>()</span><br><span class="line"> .<span class="title function_">catch</span>(<span class="function">(<span class="params">e</span>) =&gt;</span> &#123;</span><br><span class="line"> <span class="keyword">throw</span> e</span><br><span class="line"> &#125;)</span><br><span class="line"> .<span class="title function_">finally</span>(<span class="title function_">async</span> () =&gt; &#123;</span><br><span class="line"> <span class="keyword">await</span> prisma.$disconnect()</span><br><span class="line"> &#125;)</span><br></pre></td></tr></table></figure>
-<p>当执行上述查询时,Prisma <strong>不会</strong> 生成 N+1 个 SQL 查询。而是首先会分析请求,并将其转化为数量非常有限的高效 SQL 查询。对于上面这个一对多关系的 <code>include</code> 查询,Prisma 通常会执行以下<strong>两步</strong>(类似于批量加载策略):</p>
+<p>当执行上述查询时,Prisma 不会 生成 N+1 个 SQL 查询。而是首先会分析请求,并将其转化为数量非常有限的高效 SQL 查询。对于上面这个一对多关系的 <code>include</code> 查询,Prisma 通常会执行以下两步(类似于批量加载策略):</p>
<ol>
-<li><strong>查询父模型</strong>: 获取所有 <code>User</code> 记录。<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> &quot;public&quot;.&quot;User&quot;.&quot;id&quot;, &quot;public&quot;.&quot;User&quot;.&quot;name&quot;, <span class="comment">/* ... other user fields */</span> <span class="keyword">FROM</span> &quot;public&quot;.&quot;User&quot; <span class="keyword">WHERE</span> <span class="number">1</span><span class="operator">=</span><span class="number">1</span></span><br></pre></td></tr></table></figure></li>
-<li><strong>查询关联的子模型</strong>: 使用上一步获取到的所有用户 <code>id</code>,通过 <code>WHERE IN (...)</code> 子句一次性查询所有相关的 <code>Post</code> 记录。<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> &quot;public&quot;.&quot;Post&quot;.&quot;id&quot;, &quot;public&quot;.&quot;Post&quot;.&quot;title&quot;, &quot;public&quot;.&quot;Post&quot;.&quot;authorId&quot;, <span class="comment">/* ... other post fields */</span> <span class="keyword">FROM</span> &quot;public&quot;.&quot;Post&quot; <span class="keyword">WHERE</span> &quot;public&quot;.&quot;Post&quot;.&quot;authorId&quot; <span class="keyword">IN</span> ($<span class="number">1</span>, $<span class="number">2</span>, $<span class="number">3</span>, ...) <span class="comment">/* 这里的 $1, $2, ... 是第一步查到的用户 ID 列表 */</span></span><br></pre></td></tr></table></figure></li>
+<li>查询父模型: 获取所有 <code>User</code> 记录。<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> &quot;public&quot;.&quot;User&quot;.&quot;id&quot;, &quot;public&quot;.&quot;User&quot;.&quot;name&quot;, <span class="comment">/* ... other user fields */</span> <span class="keyword">FROM</span> &quot;public&quot;.&quot;User&quot; <span class="keyword">WHERE</span> <span class="number">1</span><span class="operator">=</span><span class="number">1</span></span><br></pre></td></tr></table></figure></li>
+<li>查询关联的子模型: 使用上一步获取到的所有用户 <code>id</code>,通过 <code>WHERE IN (...)</code> 子句一次性查询所有相关的 <code>Post</code> 记录。<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> &quot;public&quot;.&quot;Post&quot;.&quot;id&quot;, &quot;public&quot;.&quot;Post&quot;.&quot;title&quot;, &quot;public&quot;.&quot;Post&quot;.&quot;authorId&quot;, <span class="comment">/* ... other post fields */</span> <span class="keyword">FROM</span> &quot;public&quot;.&quot;Post&quot; <span class="keyword">WHERE</span> &quot;public&quot;.&quot;Post&quot;.&quot;authorId&quot; <span class="keyword">IN</span> ($<span class="number">1</span>, $<span class="number">2</span>, $<span class="number">3</span>, ...) <span class="comment">/* 这里的 $1, $2, ... 是第一步查到的用户 ID 列表 */</span></span><br></pre></td></tr></table></figure></li>
</ol>
<p>Prisma Client 在内存中将这两次查询的结果高效地组合起来,最终返回嵌套的、符合 TypeScript 类型的数据。</p>
<h2 id="Refs"><a href="#Refs" class="headerlink" title="Refs."></a>Refs.</h2><ul>
diff --git a/2025/04/13/uuidv7-rdbms/index.html b/2025/04/13/uuidv7-rdbms/index.html
index 1f555b75..fd9da1f6 100644
--- a/2025/04/13/uuidv7-rdbms/index.html
+++ b/2025/04/13/uuidv7-rdbms/index.html
@@ -209,7 +209,7 @@
<hr>
<p>Rimon Tawadrous 在其 GitHub repo 中的测试,对比 100 万条逐条插入实验,UUID v7 相较 UUID v4 在单线程插入上速度快约 3.24%,多线程下更可观【1】。 </p>
<hr>
-<p><strong>参考链接</strong><br>[1] “为什么 UUID 7 比 UUID 4 更适合作为 RDBMS 的聚集索引?” dbaplus.cn<br> <a target="_blank" rel="noopener" href="https://dbaplus.cn/news-160-6313-1.html">https://dbaplus.cn/news-160-6313-1.html</a><br>[2] “PostgreSQL and UUID as primary key” maciejwalkowiak<br> <a target="_blank" rel="noopener" href="https://maciejwalkowiak.com/blog/postgres-uuid-primary-key/">https://maciejwalkowiak.com/blog/postgres-uuid-primary-key/</a><br>[3] “Optimised UUIDs in mysql” stitcher<br> <a target="_blank" rel="noopener" href="https://stitcher.io/blog/optimised-uuids-in-mysql">https://stitcher.io/blog/optimised-uuids-in-mysql</a><br>[3] “Storing UUID Values in MySQL” percona<br> <a target="_blank" rel="noopener" href="https://www.percona.com/blog/store-uuid-optimized-way/">https://www.percona.com/blog/store-uuid-optimized-way/</a></p>
+<p>参考链接<br>[1] “为什么 UUID 7 比 UUID 4 更适合作为 RDBMS 的聚集索引?” dbaplus.cn<br> <a target="_blank" rel="noopener" href="https://dbaplus.cn/news-160-6313-1.html">https://dbaplus.cn/news-160-6313-1.html</a><br>[2] “PostgreSQL and UUID as primary key” maciejwalkowiak<br> <a target="_blank" rel="noopener" href="https://maciejwalkowiak.com/blog/postgres-uuid-primary-key/">https://maciejwalkowiak.com/blog/postgres-uuid-primary-key/</a><br>[3] “Optimised UUIDs in mysql” stitcher<br> <a target="_blank" rel="noopener" href="https://stitcher.io/blog/optimised-uuids-in-mysql">https://stitcher.io/blog/optimised-uuids-in-mysql</a><br>[3] “Storing UUID Values in MySQL” percona<br> <a target="_blank" rel="noopener" href="https://www.percona.com/blog/store-uuid-optimized-way/">https://www.percona.com/blog/store-uuid-optimized-way/</a></p>
</div>
diff --git a/2025/05/07/nestjs-bullmq-mail-business/index.html b/2025/05/07/nestjs-bullmq-mail-business/index.html
index 33c566c5..0315b2b9 100644
--- a/2025/05/07/nestjs-bullmq-mail-business/index.html
+++ b/2025/05/07/nestjs-bullmq-mail-business/index.html
@@ -196,14 +196,14 @@
<ol>
<li>队列(在 NestJS 里由 <code>@Processor</code> 装饰的类)会被一个底层的 <code>Worker</code> 订阅。 </li>
<li>有新任务(job)进来时,Worker 会调用写在该类里的 <code>async process(job: Job)</code> 方法。 </li>
-<li>如果 <code>process()</code> 正常返回(即没有抛异常),Job 就被标记为 <strong>completed</strong>,然后才会去触发所有注册了 <code>@OnWorkerEvent(&#39;completed&#39;)</code> 的回调。</li>
+<li>如果 <code>process()</code> 正常返回(即没有抛异常),Job 就被标记为 completed,然后才会去触发所有注册了 <code>@OnWorkerEvent(&#39;completed&#39;)</code> 的回调。</li>
</ol>
<p>也就是说:</p>
<ul>
-<li>**<code>process</code>**:是真正“干活”的地方,收到 job 之后立刻被调用,任何主业务逻辑(发邮件/写数据库/第三方请求等)都应该放这里。 </li>
-<li><strong><code>onCompleted</code><strong>:只是一个事件监听器,</strong>在 job 已经成功完成之后</strong> 才会被触发,不会影响 job 的重试逻辑(也就是说,在这里抛错,job 已经算完成了,也不会重试)。</li>
+<li><code>process</code>:是真正“干活”的地方,收到 job 之后立刻被调用,任何主业务逻辑(发邮件/写数据库/第三方请求等)都应该放这里。 </li>
+<li><code>onCompleted</code>:只是一个事件监听器,在 job 已经成功完成之后 才会被触发,不会影响 job 的重试逻辑(也就是说,在这里抛错,job 已经算完成了,也不会重试)。</li>
</ul>
-<p>而我在此处的业务目的是 “用队列来做可靠的、可重试的邮件发送”,那么<strong>一定要把发送邮件的逻辑写到 <code>process()</code> 里</strong>,这样在 <code>commandBus.execute(new SendMailCommand(...))</code> 抛错时,BullMQ 会根据创建 JOB 时的重试策略(retry、backoff 等)自动重新入队。而把它放到 <code>onCompleted()</code>,只相当于 job 成功完成后的“事后通知”,一旦失败不会再重试,也无法利用 BullMQ 的锁、超时、重试机制。</p>
+<p>而我在此处的业务目的是 “用队列来做可靠的、可重试的邮件发送”,那么一定要把发送邮件的逻辑写到 <code>process()</code> 里,这样在 <code>commandBus.execute(new SendMailCommand(...))</code> 抛错时,BullMQ 会根据创建 JOB 时的重试策略(retry、backoff 等)自动重新入队。而把它放到 <code>onCompleted()</code>,只相当于 job 成功完成后的“事后通知”,一旦失败不会再重试,也无法利用 BullMQ 的锁、超时、重试机制。</p>
<p>举个最简化的调整示例,删掉 <code>onCompleted</code>,把真正的发信放到 <code>process</code>: </p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line">// ... existing imports ...</span><br><span class="line"></span><br><span class="line">@Processor(process.env.MAILER_QUEUE_NAME || &quot;gcpm-mailer&quot;)</span><br><span class="line">export class BullMQMailerProcesser extends WorkerHost &#123;</span><br><span class="line"> constructor(</span><br><span class="line"> private readonly commandBus: CommandBus,</span><br><span class="line"> private readonly logger: LoggingService,</span><br><span class="line"> ) &#123;</span><br><span class="line"> super();</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> // ① 当有新 job 拉取到时,这个方法会被调用</span><br><span class="line"> public async process(job: Job): Promise&lt;void&gt; &#123;</span><br><span class="line"> const mailAggregate = new Mail(job.data.mail);</span><br><span class="line"> try &#123;</span><br><span class="line"> await this.commandBus.execute(new SendMailCommand(mailAggregate));</span><br><span class="line"> &#125; catch (err) &#123;</span><br><span class="line"> this.logger.error(`邮件发送失败,jobId=$&#123;job.id&#125;`, err);</span><br><span class="line"> // 抛出错误,触发重试或失败</span><br><span class="line"> throw err;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> // ② onCompleted 仅在 process() 正常返回后触发,</span><br><span class="line"> // 不建议在这里执行核心业务(也无法触发重试)。</span><br><span class="line"> // @OnWorkerEvent(&quot;completed&quot;)</span><br><span class="line"> // async onCompleted(job: Job) &#123; … &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
@@ -213,9 +213,9 @@
<li>“Events → OnJobCompleted”:completed 事件只是一个监听钩子,不会参与重试。</li>
</ul>
<hr>
-<p>而 <strong>重试次数本身并没有一个硬性上限</strong>,完全由添加 Job 时通过 <code>attempts</code> 这个选项来控制:</p>
+<p>而 重试次数本身并没有一个硬性上限,完全由添加 Job 时通过 <code>attempts</code> 这个选项来控制:</p>
<ul>
-<li>默认情况下,如果不传 <code>attempts</code>(或不在 <code>defaultJobOptions</code> 里配置),Job <strong>不会自动重试</strong>(相当于 <code>attempts = 0</code>)。 </li>
+<li>默认情况下,如果不传 <code>attempts</code>(或不在 <code>defaultJobOptions</code> 里配置),Job 不会自动重试(相当于 <code>attempts = 0</code>)。 </li>
<li>如果在 <code>queue.add()</code>(或全局 <code>defaultJobOptions</code>)里设置了 <code>attempts: N</code>,那么 BullMQ 最多会让该 Job 运行 N 次(也就是初始执行 + N−1 次重试,或者根据文档含义最多触发 N 次失败) ,失败后才算真正移入失败集合。 </li>
<li><code>attempts</code> 可以是任意的正整数(受 JavaScript <code>Number</code> 范围限制),BullMQ 本身不会再做额外的上限检查。</li>
</ul>
@@ -224,13 +224,13 @@
<hr>
<p>还有一个需要注意的地方,在我的业务中,邮件发送的是一种时间区间报告,这个报告包含了过去二十四小时的一些系统中的事件,但如果重试有延迟策略或重试本身就有计算成本的话,这封邮件就不是 “过去二十四小时” 的了,因为重试带来了一个真空期。</p>
-<p>换言之,这个问题本质上是——<strong>重试导致「发送时刻」与「原始 24 小时窗口」错开</strong>,从而让邮件里报出来的数据不再精确。常见的解决思路就是:<strong>把「窗口定义」或者「报表内容」在调度时就固化下来,真正的队列任务只负责发送</strong>,而不再实时去重新计算时间区间。</p>
+<p>换言之,这个问题本质上是——重试导致「发送时刻」与「原始 24 小时窗口」错开,从而让邮件里报出来的数据不再精确。常见的解决思路就是:把「窗口定义」或者「报表内容」在调度时就固化下来,真正的队列任务只负责发送,而不再实时去重新计算时间区间。</p>
<p>我想到了两种解决方案:</p>
<p>一、任务参数里带上「时间区间」<br> 在 enqueue 的时候,就算出 windowStart&#x2F;windowEnd,然后把它放到 <code>job.data</code> 里。无论后面 <code>process</code> 什么时候真正跑,都是基于同一个时间区间去查询:</p>
<figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 调度时</span></span><br><span class="line"><span class="keyword">const</span> now = <span class="keyword">new</span> <span class="title class_">Date</span>();</span><br><span class="line"><span class="keyword">const</span> windowStart = <span class="keyword">new</span> <span class="title class_">Date</span>(now.<span class="title function_">getTime</span>() - <span class="number">24</span> * <span class="number">60</span> * <span class="number">60</span> * <span class="number">1000</span>);</span><br><span class="line"><span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">mailerQueue</span>.<span class="title function_">add</span>(</span><br><span class="line"> id,</span><br><span class="line"> &#123;</span><br><span class="line"> <span class="attr">mail</span>: <span class="keyword">new</span> <span class="title class_">Mail</span>(&#123;</span><br><span class="line"> ...options,</span><br><span class="line"> id,</span><br><span class="line"> <span class="attr">sentAt</span>: now,</span><br><span class="line"> <span class="attr">status</span>: <span class="title class_">MailStatus</span>.<span class="property">PENDING</span>,</span><br><span class="line"> windowStart,</span><br><span class="line"> <span class="attr">windowEnd</span>: now,</span><br><span class="line"> &#125;),</span><br><span class="line"> &#125;,</span><br><span class="line"> &#123;</span><br><span class="line"> <span class="attr">attempts</span>: <span class="number">3</span>,</span><br><span class="line"> <span class="attr">backoff</span>: &#123; <span class="attr">type</span>: <span class="string">&#x27;exponential&#x27;</span>, <span class="attr">delay</span>: <span class="number">1000</span> &#125;,</span><br><span class="line"> &#125;,</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">// process 里</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">async</span> <span class="title function_">process</span>(<span class="params"><span class="attr">job</span>: <span class="title class_">Job</span></span>) &#123;</span><br><span class="line"> <span class="keyword">const</span> &#123; windowStart, windowEnd &#125; = job.<span class="property">data</span>.<span class="property">mail</span>;</span><br><span class="line"> <span class="comment">// ① 只查询 [windowStart, windowEnd] 的事件</span></span><br><span class="line"> <span class="keyword">const</span> events = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">reportService</span>.<span class="title function_">findEvents</span>(windowStart, windowEnd);</span><br><span class="line"> <span class="keyword">const</span> reportHtml = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">reportService</span>.<span class="title function_">renderReport</span>(events);</span><br><span class="line"> <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">commandBus</span>.<span class="title function_">execute</span>(<span class="keyword">new</span> <span class="title class_">SendMailCommand</span>(job.<span class="property">data</span>.<span class="property">mail</span>, reportHtml));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p> ➜ 这样无是马上执行还是几次重试后才执行,数据规则都不会变。</p>
<p>二、预先生成「静态报表内容」,挂到队列里<br> 如果计算成本很高,或者怕重复查询数据开销大,也可以在调度时就把最终的 HTML&#x2F;Text&#x2F;附件 都先打好,然后作为 <code>job.data</code> 传进去,真正的 <code>process()</code> 只做一次“发送”即可:<br> <figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 调度时:先生成报告</span></span><br><span class="line"><span class="keyword">const</span> now = <span class="keyword">new</span> <span class="title class_">Date</span>();</span><br><span class="line"><span class="keyword">const</span> windowStart = <span class="keyword">new</span> <span class="title class_">Date</span>(now.<span class="title function_">getTime</span>() - <span class="number">24</span>*<span class="number">3600</span>*<span class="number">1000</span>);</span><br><span class="line"><span class="keyword">const</span> events = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">reportService</span>.<span class="title function_">findEvents</span>(windowStart, now);</span><br><span class="line"><span class="keyword">const</span> reportHtml = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">reportService</span>.<span class="title function_">renderReport</span>(events);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 把静态内容塞到队列</span></span><br><span class="line"><span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">mailerQueue</span>.<span class="title function_">add</span>(</span><br><span class="line"> id,</span><br><span class="line"> &#123;</span><br><span class="line"> <span class="attr">mail</span>: <span class="keyword">new</span> <span class="title class_">Mail</span>(&#123; <span class="comment">/*…*/</span>, windowStart, <span class="attr">windowEnd</span>: now &#125;),</span><br><span class="line"> reportHtml, <span class="comment">// &lt;- 预渲染好的文本/HTML</span></span><br><span class="line"> <span class="attr">attachments</span>: […], <span class="comment">// &lt;- 如果有附件也一并塞</span></span><br><span class="line"> &#125;,</span><br><span class="line"> &#123; <span class="attr">attempts</span>: <span class="number">3</span>, <span class="attr">backoff</span>: &#123; <span class="attr">type</span>: <span class="string">&#x27;fixed&#x27;</span>, <span class="attr">delay</span>: <span class="number">5_000</span> &#125; &#125;,</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">// process 里只关注发送</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">async</span> <span class="title function_">process</span>(<span class="params"><span class="attr">job</span>: <span class="title class_">Job</span></span>) &#123;</span><br><span class="line"> <span class="keyword">try</span> &#123;</span><br><span class="line"> <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">mailService</span>.<span class="title function_">send</span>(&#123;</span><br><span class="line"> <span class="attr">to</span>: job.<span class="property">data</span>.<span class="property">mail</span>.<span class="property">to</span>,</span><br><span class="line"> <span class="attr">subject</span>: <span class="string">`系统 24h 报表`</span>,</span><br><span class="line"> <span class="attr">html</span>: job.<span class="property">data</span>.<span class="property">reportHtml</span>,</span><br><span class="line"> <span class="attr">attachments</span>: job.<span class="property">data</span>.<span class="property">attachments</span>,</span><br><span class="line"> &#125;);</span><br><span class="line"> &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line"> <span class="keyword">throw</span> e; <span class="comment">// 触发重试</span></span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><br> ➜ 重试带来的任何延迟,都不影响邮件正文,始终是一份「事先约定好、并且静态化」的报告。</p>
-<p>这两种模式都能保证<strong>最终发送时的数据窗口</strong>或<strong>内容</strong>,与当初调度时的预期完全一致,不会因为重试延迟而出现“数据真空”或“多算&#x2F;少算”问题。</p>
+<p>这两种模式都能保证最终发送时的数据窗口或内容,与当初调度时的预期完全一致,不会因为重试延迟而出现“数据真空”或“多算&#x2F;少算”问题。</p>
<h2 id="参考文档:"><a href="#参考文档:" class="headerlink" title="参考文档:"></a>参考文档:</h2><ul>
<li>“Retrying failing jobs” · BullMQ Guide<br><a target="_blank" rel="noopener" href="https://docs.bullmq.io/guide/retrying-failing-jobs">https://docs.bullmq.io/guide/retrying-failing-jobs</a></li>
<li>BullMQ Guide &amp; Patterns · Process Step Jobs (completed event only fires after process resolves)<br><a target="_blank" rel="noopener" href="https://docs.bullmq.io/patterns/process-step-jobs">https://docs.bullmq.io/patterns/process-step-jobs</a></li>
diff --git a/2025/05/08/Multiplayer-Collaborative-Systems-tips/index.html b/2025/05/08/Multiplayer-Collaborative-Systems-tips/index.html
index 27e0b74a..1838c49a 100644
--- a/2025/05/08/Multiplayer-Collaborative-Systems-tips/index.html
+++ b/2025/05/08/Multiplayer-Collaborative-Systems-tips/index.html
@@ -194,7 +194,7 @@
<div class="post-content">
<p>最近碰到一块业务:在系统中可以存在多个用户同时对某个项目信息进行编辑,这种多人协作的场景挺有意思的,不过在我们的业务中,并不需要实时协作,只需要保证不会出错就行,话虽如此,但也可以探索一下实时协作的实现方案,防止老年痴呆。</p>
<p>先来看看第一个方案 —— CRDT(Conflict-free Replicated Data Type,无冲突可复制数据类型)是一类数据结构,它保证了在分布式节点(或多客户端)上进行离线&#x2F;并发更新后,无需中心协调、也无需人工干预,通过“合并策略”就能得到一致的最终状态。 </p>
-<p>核心思想是:所有并发操作都是<strong>幂等</strong>(idempotent)、<strong>可交换</strong>(commutative)的。</p>
+<p>核心思想是:所有并发操作都是幂等(idempotent)、可交换(commutative)的。</p>
<p>常见类型有:<br>一、G-Counter(只能增计数器)<br>二、PN-Counter(可增可减计数器)<br>三、LWW-Register(最后写入胜出)<br>四、结合 JSON 的树型 CRDT(如 <a target="_blank" rel="noopener" href="https://github.com/automerge/automerge">Automerge</a> &#x2F; <a target="_blank" rel="noopener" href="https://yjs.dev/">Yjs</a>) </p>
<p>更多原理可参考 Decipad 博客“Collaborative and Offline Editing Using CRDTs”[^1]。</p>
<blockquote>
@@ -237,7 +237,7 @@ await prisma.$transaction(async tx =&gt; &#123;
<p>前端的话,大概就是:</p>
<p>在进入编辑前请求一下 <code>/api/project/:id/lock</code> 之类的 API,失败则提示“被占用”;<br>在 <code>onbeforeunload</code> 时执行 <code>/unlock</code>;<br>超时后后端自动允许新锁。 </p>
<hr>
-<p>第二个方案是乐观并发控制(Optimistic Concurrency) :记录资源的<strong>版本号</strong>或<strong>时间戳</strong>;客户端提交更新时带上自己的版本号,后端检查版本是否一致,不一致则认为冲突,返回 409,由客户端告知用户“数据已过期,请刷新后合并”。 </p>
+<p>第二个方案是乐观并发控制(Optimistic Concurrency) :记录资源的版本号或时间戳;客户端提交更新时带上自己的版本号,后端检查版本是否一致,不一致则认为冲突,返回 409,由客户端告知用户“数据已过期,请刷新后合并”。 </p>
<p>具体实现中,可以尝试在 <code>project</code> 表加上 <code>version INT NOT NULL DEFAULT 1, updated_at TIMESTAMPTZ</code> ,然后更新项目时:</p>
<pre><code class="ts">async updateProject(parent, &#123; id, version, input &#125;, ctx) &#123;
const result = await prisma.$executeRaw`
@@ -257,7 +257,7 @@ await prisma.$transaction(async tx =&gt; &#123;
<p>前端捕获到冲突错误可以用一个弹窗提示“另有用户已更新此项目,是否合并&#x2F;重新加载?” 之类的玩意儿。</p>
<hr>
<p>第三个方案是:操作转化(Operational Transformation,OT) </p>
-<p>也就是记录用户每次的“操作”(insert&#x2F;delete at position),服务器根据历史操作序列对并发操作做<strong>转化</strong>(transform),确保先到达的操作调整后再应用后到达的。 </p>
+<p>也就是记录用户每次的“操作”(insert&#x2F;delete at position),服务器根据历史操作序列对并发操作做转化(transform),确保先到达的操作调整后再应用后到达的。 </p>
<p>有一些实现案例:<br>一、ShareDB(Node.js)<br>二、Google Docs 中的同步算法 </p>
<p>具体实现的话,可能要现在前端逐字符&#x2F;块地包装成操作并 WebSocket 推送,服务器再维护一个“操作历史队列”,每来一个 op 就 transform 并 broadcast,而客户端收到广播后,按顺序 replay 保证视图一致。</p>
<hr>
@@ -267,7 +267,7 @@ await prisma.$transaction(async tx =&gt; &#123;
<hr>
<p>总结来说,</p>
<ul>
-<li>CRDT 最擅长 <strong>去中心化</strong>、<strong>离线编辑</strong>、<strong>自动合并</strong>; </li>
+<li>CRDT 最擅长 去中心化、离线编辑、自动合并; </li>
<li>若不引入 CRDT,可根据业务侧重点选用: <ol>
<li>悲观锁 → 强制串行编辑,简单粗暴; </li>
<li>乐观并发 → 适合大多数业务场景,成本低; </li>
diff --git a/2025/05/27/vertical-slicing-practice/index.html b/2025/05/27/vertical-slicing-practice/index.html
index 325c7a76..2c73a860 100644
--- a/2025/05/27/vertical-slicing-practice/index.html
+++ b/2025/05/27/vertical-slicing-practice/index.html
@@ -192,43 +192,43 @@
</div>
</div>
<div class="post-content">
- <p><strong>垂直切片 (Vertical Slicing)</strong> 是一种在敏捷软件开发中将产品需求(通常是用户故事)拆分为可独立交付的、具有端到端功能的小块的方法。这意味着每个“切片”都包含了从用户界面 (UI) 到底层数据库,以及中间所有业务逻辑层所需的工作。</p>
+ <p>垂直切片 (Vertical Slicing) 是一种在敏捷软件开发中将产品需求(通常是用户故事)拆分为可独立交付的、具有端到端功能的小块的方法。这意味着每个“切片”都包含了从用户界面 (UI) 到底层数据库,以及中间所有业务逻辑层所需的工作。</p>
<p>与水平切片(即按技术分层,如先完成所有 UI,再完成所有后端逻辑)不同,垂直切片的目标是尽快交付一个虽小但完整可用的功能。</p>
<hr>
<p>想象一个蛋糕,垂直切片就像切下一块完整的蛋糕,包含从顶部到底部的每一层。在软件开发中,这意味着一个任务或用户故事的完成会涉及到:</p>
<ul>
-<li>**用户界面 (UI)**:用户能看到并与之交互的部分。</li>
-<li>**业务逻辑层 (Business Logic Layer)**:处理数据和执行核心功能的部分。</li>
-<li>**数据访问层 (Data Access Layer)**:与数据库或其他数据存储交互的部分。</li>
-<li>**数据库 (Database)**:存储数据的部分。</li>
+<li>用户界面 (UI):用户能看到并与之交互的部分。</li>
+<li>业务逻辑层 (Business Logic Layer):处理数据和执行核心功能的部分。</li>
+<li>数据访问层 (Data Access Layer):与数据库或其他数据存储交互的部分。</li>
+<li>数据库 (Database):存储数据的部分。</li>
</ul>
<p>所以一个垂直切片代表了一个可以独立运行、测试和向用户展示的小功能。</p>
<hr>
<p>在实践中,用垂直切片进行任务拆分或许可以按照如下步骤来实现:</p>
-<p>一、**从用户故事开始 (Start with User Stories)**:明确用户需要什么功能以及这个功能为用户带来的价值。例如:“作为一个注册用户,我希望能用我的邮箱和密码登录系统,以便访问我的个人资料。”</p>
-<p>二、<strong>识别涉及的技术层面 (Identify Affected Layers)</strong></p>
+<p>一、从用户故事开始 (Start with User Stories):明确用户需要什么功能以及这个功能为用户带来的价值。例如:“作为一个注册用户,我希望能用我的邮箱和密码登录系统,以便访问我的个人资料。”</p>
+<p>二、识别涉及的技术层面 (Identify Affected Layers)</p>
<p>对于登录功能,需要考虑:</p>
<ul>
-<li><strong>UI 层</strong>:登录表单(输入邮箱、密码的地方)、提交按钮、错误提示信息。</li>
-<li><strong>API&#x2F;服务层</strong>:接收登录请求、验证用户凭证的接口。</li>
-<li><strong>业务逻辑层</strong>:校验输入格式、查询用户信息、验证密码、生成会话(Session)或令牌(Token)。</li>
-<li><strong>数据访问层</strong>:从数据库中读取用户信息。</li>
+<li>UI 层:登录表单(输入邮箱、密码的地方)、提交按钮、错误提示信息。</li>
+<li>API&#x2F;服务层:接收登录请求、验证用户凭证的接口。</li>
+<li>业务逻辑层:校验输入格式、查询用户信息、验证密码、生成会话(Session)或令牌(Token)。</li>
+<li>数据访问层:从数据库中读取用户信息。</li>
</ul>
-<p>三、<strong>创建可交付的小功能块 (Create Small, Deliverable Chunks)</strong></p>
+<p>三、创建可交付的小功能块 (Create Small, Deliverable Chunks)</p>
<p>就是将一个大的用户故事拆分成更小的、但仍然是垂直的、可独立交付的故事。例如,可以将“用户登录”进一步细化:</p>
-<p>**切片1 (基础登录)**:用户可以使用正确的邮箱和密码成功登录。这包含了 UI 输入、后端验证和数据库查询。</p>
-<p>**切片2 (错误处理)**:用户输入错误的邮箱或密码时,系统给出明确的错误提示。这可能只涉及 UI 和业务逻辑层的少量修改。</p>
-<p>**切片3 (“记住我” 功能)**:用户可以选择“记住我”,下次访问时自动登录。这可能涉及 UI、业务逻辑和客户端存储。</p>
-<p>四、**确保每个切片都有价值 (Ensure Each Slice Has Value)**:每完成一个切片,都应该为用户或产品带来可感知的价值,并且理想情况下是可以演示给利益相关者看的。</p>
-<p>五、**保持切片足够小 (Keep Slices Small Enough)**:每个切片的工作量应该小到可以在一个迭代周期(例如 Sprint)内完成。这有助于团队保持专注,并快速获得反馈。</p>
+<p>切片1 (基础登录):用户可以使用正确的邮箱和密码成功登录。这包含了 UI 输入、后端验证和数据库查询。</p>
+<p>切片2 (错误处理):用户输入错误的邮箱或密码时,系统给出明确的错误提示。这可能只涉及 UI 和业务逻辑层的少量修改。</p>
+<p>切片3 (“记住我” 功能):用户可以选择“记住我”,下次访问时自动登录。这可能涉及 UI、业务逻辑和客户端存储。</p>
+<p>四、确保每个切片都有价值 (Ensure Each Slice Has Value):每完成一个切片,都应该为用户或产品带来可感知的价值,并且理想情况下是可以演示给利益相关者看的。</p>
+<p>五、保持切片足够小 (Keep Slices Small Enough):每个切片的工作量应该小到可以在一个迭代周期(例如 Sprint)内完成。这有助于团队保持专注,并快速获得反馈。</p>
<hr>
<p>这里有一些拆分技巧:</p>
<ul>
-<li><strong>按操作流程拆分</strong>:例如,一个复杂的表单提交可以先实现基本信息的提交,后续再添加高级选项的提交。</li>
-<li><strong>按业务规则拆分</strong>:先实现核心的业务规则,再逐步添加次要的或复杂的规则。</li>
-<li><strong>按数据类型或参数拆分</strong>:先支持一种数据类型或最常用的参数,再扩展到其他类型。</li>
-<li><strong>按用户角色或权限拆分</strong>:先实现某个核心角色的功能,再实现其他角色的特定功能。</li>
-<li><strong>简化错误处理或用户体验</strong>:先实现基本功能,再完善错误处理和用户体验细节。</li>
+<li>按操作流程拆分:例如,一个复杂的表单提交可以先实现基本信息的提交,后续再添加高级选项的提交。</li>
+<li>按业务规则拆分:先实现核心的业务规则,再逐步添加次要的或复杂的规则。</li>
+<li>按数据类型或参数拆分:先支持一种数据类型或最常用的参数,再扩展到其他类型。</li>
+<li>按用户角色或权限拆分:先实现某个核心角色的功能,再实现其他角色的特定功能。</li>
+<li>简化错误处理或用户体验:先实现基本功能,再完善错误处理和用户体验细节。</li>
</ul>
</div>