<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Pure-Function on Tarragon</title><link>https://tarrragon.github.io/blog/tags/pure-function/</link><description>Recent content in Pure-Function on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 10 Jul 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/pure-function/index.xml" rel="self" type="application/rss+xml"/><item><title>「該收多少錢」抽成 pure function — IO 在邊界、領域計算在核心</title><link>https://tarrragon.github.io/blog/work-log/dart_unsettled_cart_pure_function/</link><pubDate>Fri, 10 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/work-log/dart_unsettled_cart_pure_function/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>觸發場景&lt;/strong>：POS 的提前結帳功能讓一個資料語意浮出來：後端購物車明細的 &lt;code>quantity&lt;/code> 是總量、包含客人已經先結過帳的份數。桌位詳情、掛單列表、取單頁都要顯示「還要處理 / 還要收」的數字——用總量顯示，員工會誤收已付過的錢
&lt;strong>疑問來源&lt;/strong>：這段「扣掉已結帳份數、算出應付金額」的邏輯，好幾個畫面都要用，該放在哪？
&lt;strong>整理目的&lt;/strong>：記下領域計算抽成 pure function 的做法、以及這個函式裡三個容易寫錯的細節
&lt;strong>本文邊界&lt;/strong>：素材是該專案現行的 &lt;code>unsettledCartView&lt;/code> 實作與註解；「pure function + caller 餵資料」的形態可遷移、折扣規則是這個 domain 當下的切法&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="形態一個函式輸入齊備輸出結構化">形態：一個函式、輸入齊備、輸出結構化&lt;/h2>
&lt;p>整段計算收在一個頂層函式裡，簽名把設計講完了：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="n">List&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">OrderedCartItem&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">items&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="n">Money&lt;/span> &lt;span class="n">originalAmount&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 折扣前商品總額（含口味加價）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">Money&lt;/span> &lt;span class="n">discountAmount&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 折扣總額（單品 + 整筆）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">Money&lt;/span> &lt;span class="n">totalAmount&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 應付金額（不會為負）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">totalItemCount&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="n">unsettledCartView&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="kd">required&lt;/span> &lt;span class="n">ShoppingCart&lt;/span> &lt;span class="n">cart&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="kd">required&lt;/span> &lt;span class="n">List&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">RemoteOrderSnapshotItem&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">snapshotItems&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="n">Money&lt;/span>&lt;span class="o">?&lt;/span> &lt;span class="n">manualCartDiscount&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="p">})&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>關鍵的選擇是&lt;strong>它不做任何 IO&lt;/strong>。已結帳份數要靠線上點單的 snapshot 判斷，而 snapshot 在 repository 裡——函式不去拿，由 caller 先從 &lt;code>IOnlineOrderRepository.itemsByCart&lt;/code> 取好傳進來。這一刀切出兩個直接的好處：測試就是「餵資料、斷言輸出」（不用 mock repository）；任何畫面都能重用（桌位詳情、掛單列表、取單頁各自拿自己的資料餵）。金額回三個欄位而不是一個，因為 UI 要顯示折扣明細——輸出的形狀由消費者的需求決定、用 record 一次回齊。&lt;/p>
&lt;h2 id="細節一合併鍵要跟同一品項的定義同維度">細節一：合併鍵要跟「同一品項」的定義同維度&lt;/h2>
&lt;p>計算的第一步是把同品項的多筆 detail 合併、第二步用已結帳份數扣減。扣減需要一個 key 來對應「snapshot 裡結掉的」跟「cart 裡剩下的」——這個 key 的維度是實作裡最容易寫錯的地方，原始註解直接記了陷阱：&lt;/p>
&lt;blockquote>
&lt;p>只用 spec 當 key 的話，同 spec 不同 customizations 的多筆 cart 項目會被同一筆已結帳數量重複扣減，造成新加的不同口味品項被誤過濾。&lt;/p>&lt;/blockquote>
&lt;p>正解是 key 用「規格 + 客製化簽章」——跟 &lt;code>CartItem.isSameItem&lt;/code> 對「同一品項」的判定維度&lt;strong>一致&lt;/strong>。原則化：&lt;strong>扣減 / 合併 / 對帳用的 key，維度必須等於這個 domain 對「同一個」的定義&lt;/strong>，少一個維度就會把不同的東西誤當同一個。還有一個資料現實的妥協記在註解裡：snapshot 的客製化只有 name 沒有 id，所以簽章用排序後的 name 串接——妥協可以，但要寫明它是妥協。&lt;/p>
&lt;h2 id="細節二兩層折扣clamp-的上限要選對">細節二：兩層折扣、clamp 的上限要選對&lt;/h2>
&lt;p>折扣有兩層：單品折扣（記在每個 &lt;code>CartItem.discount&lt;/code>、由改價彈窗設定）跟整筆折扣（結帳頁折價按鈕、業務上不分攤回品項）。整筆折扣的 clamp 邊界又是一個註解裡的判讀：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 上限用 itemsSubtotal 而非 originalAmount，避免單品折扣 + 整筆折扣
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">// 加總超過商品總額導致應付為負。
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">final&lt;/span> &lt;span class="n">cartDiscountAmount&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">rawCartDiscount&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">itemsSubtotal&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="n">itemsSubtotal&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="p">...;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>直覺會拿「商品原始總額」當上限，但單品折扣已經先扣了一輪——整筆折扣的真正可用空間是&lt;strong>扣完單品折扣後的餘額&lt;/strong>。兩層折扣各自對自己的空間 clamp，「應付金額不會為負」這個輸出契約才成立。多層減項的通用判讀：每一層的邊界要對「前面各層扣剩的餘額」算、不對原始值算。&lt;/p>
&lt;h2 id="細節三擴充點先設計但不先實作">細節三：擴充點先設計、但不先實作&lt;/h2>
&lt;p>未來的折扣規則（會員等級、促銷、優惠券、滿額減）還沒被後端定義，函式的註解預留了接入方式：規則成形時新增一個並列的 pure function（&lt;code>cartDiscountView&lt;/code>），規則需要的 IO 資料同樣由 caller 拿好傳入，本函式在算完品項後呼叫它、把結果套進三個金額欄位——&lt;strong>本函式與所有 UI 都不需要改&lt;/strong>。&lt;/p>
&lt;p>這跟&lt;a href="https://tarrragon.github.io/blog/work-log/flutter_async_query_overdesign_oscillation/" data-link-title="同一個子系統膨脹兩次：異步查詢系統的過度設計震盪" data-link-desc="過度設計會復發、且兩輪的機制不同：設計期的膨脹來自想像的需求（別層已處理的重試、用不到的優先級佇列），迭代期的膨脹來自不刪的舊版本（三個實作並存、狀態多處追蹤）。偽需求的檢驗法是問「這個能力已經有別層在做嗎」。">過度設計震盪&lt;/a>裡「先做起來放」的偽需求處置形成正面對照：擴充點的設計成本是一段註解（接入位置、簽章形式、啟用點），實作成本是零——它把「未來怎麼接」的思考結果留下來、把「現在就寫」的浪費留在門外。同樣的態度也出現在整筆折扣的來源上：「商業邏輯尚未由後端定義，此處先以前端 model 自洽為主；未來後端開接口時 caller 改傳後端值即可」。&lt;/p>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;ul>
&lt;li>同一段「從資料算出畫面要的數字」邏輯散在多個 widget / controller 裡——抽 pure function 的時機，重複第二次就該動手&lt;/li>
&lt;li>領域計算的測試裡出現 repository mock——計算跟 IO 黏在一起了，把資料取得推給 caller、測試立刻減重&lt;/li>
&lt;li>合併或對帳結果「偶爾多扣 / 少一筆」——查 key 的維度是否少於 domain 對「同一個」的定義&lt;/li>
&lt;li>多層折扣 / 費用的邊界檢查全部對原始總額算——疊加後可能穿底，逐層對餘額 clamp&lt;/li>
&lt;/ul>
&lt;h2 id="相關閱讀">相關閱讀&lt;/h2>
&lt;ul>
&lt;li>同專案的資料語意背景：&lt;a href="https://tarrragon.github.io/blog/work-log/pos_table_cart_lifecycle_decoupling/" data-link-title="桌子跟購物車是兩個聚合 — 從「提前結帳」推導生命週期解耦" data-link-desc="兩個業務資源該綁死成一對一、還是解耦成獨立生命週期加綁定關係——判準是有沒有業務操作需要其中一方獨立存活。以 POS 的提前結帳、純佔桌、外賣單推導桌位與購物車的聚合邊界，含組合空間大於業務空間時的非法組合封鎖。">桌子跟購物車是兩個聚合&lt;/a>——提前結帳的生命週期設計是本文「份數扣減」需求的來源；&lt;a href="https://tarrragon.github.io/blog/work-log/dart_pos_item_four_lifecycle_models/" data-link-title="同一個品項、四個 model — value object 什麼時候該升級成 entity" data-link-desc="同一個業務概念要不要拆成多個 model、value object 什麼時候該升級成 entity——判準是操作需不需要 identity-based 回寫。以 POS 品項從點選、掛單、結算到歷史訂單的四階段模型為例，含 snapshot 與 live reference 的凍結時機。">同一個品項、四個 model&lt;/a>——合併產出的 &lt;code>OrderedCartItem&lt;/code> 與 &lt;code>sourceDetailIds&lt;/code> 的身份設計&lt;/li>
&lt;li>概念地基：&lt;a href="https://tarrragon.github.io/blog/ddd/" data-link-title="DDD 領域驅動設計指南" data-link-desc="領域模型的理論與判準層：一袋欄位還是領域模型、什麼時候值得建 entity、不變式該落在哪一層強制、狀態轉換怎麼留下稽核軌跡、建構路徑怎麼設計。語言無關，實作限制路由到各語言模組。">DDD 領域驅動設計指南&lt;/a>——領域計算的歸屬、以及「從操作推導領域」（提前結帳這個操作逼出了整個 view 函式）&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<blockquote>
<p><strong>觸發場景</strong>：POS 的提前結帳功能讓一個資料語意浮出來：後端購物車明細的 <code>quantity</code> 是總量、包含客人已經先結過帳的份數。桌位詳情、掛單列表、取單頁都要顯示「還要處理 / 還要收」的數字——用總量顯示，員工會誤收已付過的錢
<strong>疑問來源</strong>：這段「扣掉已結帳份數、算出應付金額」的邏輯，好幾個畫面都要用，該放在哪？
<strong>整理目的</strong>：記下領域計算抽成 pure function 的做法、以及這個函式裡三個容易寫錯的細節
<strong>本文邊界</strong>：素材是該專案現行的 <code>unsettledCartView</code> 實作與註解；「pure function + caller 餵資料」的形態可遷移、折扣規則是這個 domain 當下的切法</p></blockquote>
<hr>
<h2 id="形態一個函式輸入齊備輸出結構化">形態：一個函式、輸入齊備、輸出結構化</h2>
<p>整段計算收在一個頂層函式裡，簽名把設計講完了：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="p">({</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="n">List</span><span class="o">&lt;</span><span class="n">OrderedCartItem</span><span class="o">&gt;</span> <span class="n">items</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="n">Money</span> <span class="n">originalAmount</span><span class="p">,</span>   <span class="c1">// 折扣前商品總額（含口味加價）
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span>  <span class="n">Money</span> <span class="n">discountAmount</span><span class="p">,</span>   <span class="c1">// 折扣總額（單品 + 整筆）
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span>  <span class="n">Money</span> <span class="n">totalAmount</span><span class="p">,</span>      <span class="c1">// 應付金額（不會為負）
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span>  <span class="kt">int</span> <span class="n">totalItemCount</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="p">})</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">unsettledCartView</span><span class="p">({</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="kd">required</span> <span class="n">ShoppingCart</span> <span class="n">cart</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="kd">required</span> <span class="n">List</span><span class="o">&lt;</span><span class="n">RemoteOrderSnapshotItem</span><span class="o">&gt;</span> <span class="n">snapshotItems</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="n">Money</span><span class="o">?</span> <span class="n">manualCartDiscount</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><p>關鍵的選擇是<strong>它不做任何 IO</strong>。已結帳份數要靠線上點單的 snapshot 判斷，而 snapshot 在 repository 裡——函式不去拿，由 caller 先從 <code>IOnlineOrderRepository.itemsByCart</code> 取好傳進來。這一刀切出兩個直接的好處：測試就是「餵資料、斷言輸出」（不用 mock repository）；任何畫面都能重用（桌位詳情、掛單列表、取單頁各自拿自己的資料餵）。金額回三個欄位而不是一個，因為 UI 要顯示折扣明細——輸出的形狀由消費者的需求決定、用 record 一次回齊。</p>
<h2 id="細節一合併鍵要跟同一品項的定義同維度">細節一：合併鍵要跟「同一品項」的定義同維度</h2>
<p>計算的第一步是把同品項的多筆 detail 合併、第二步用已結帳份數扣減。扣減需要一個 key 來對應「snapshot 裡結掉的」跟「cart 裡剩下的」——這個 key 的維度是實作裡最容易寫錯的地方，原始註解直接記了陷阱：</p>
<blockquote>
<p>只用 spec 當 key 的話，同 spec 不同 customizations 的多筆 cart 項目會被同一筆已結帳數量重複扣減，造成新加的不同口味品項被誤過濾。</p></blockquote>
<p>正解是 key 用「規格 + 客製化簽章」——跟 <code>CartItem.isSameItem</code> 對「同一品項」的判定維度<strong>一致</strong>。原則化：<strong>扣減 / 合併 / 對帳用的 key，維度必須等於這個 domain 對「同一個」的定義</strong>，少一個維度就會把不同的東西誤當同一個。還有一個資料現實的妥協記在註解裡：snapshot 的客製化只有 name 沒有 id，所以簽章用排序後的 name 串接——妥協可以，但要寫明它是妥協。</p>
<h2 id="細節二兩層折扣clamp-的上限要選對">細節二：兩層折扣、clamp 的上限要選對</h2>
<p>折扣有兩層：單品折扣（記在每個 <code>CartItem.discount</code>、由改價彈窗設定）跟整筆折扣（結帳頁折價按鈕、業務上不分攤回品項）。整筆折扣的 clamp 邊界又是一個註解裡的判讀：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 上限用 itemsSubtotal 而非 originalAmount，避免單品折扣 + 整筆折扣
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">// 加總超過商品總額導致應付為負。
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kd">final</span> <span class="n">cartDiscountAmount</span> <span class="o">=</span> <span class="n">rawCartDiscount</span> <span class="o">&gt;</span> <span class="n">itemsSubtotal</span> <span class="o">?</span> <span class="n">itemsSubtotal</span> <span class="o">:</span> <span class="p">...;</span></span></span></code></pre></div><p>直覺會拿「商品原始總額」當上限，但單品折扣已經先扣了一輪——整筆折扣的真正可用空間是<strong>扣完單品折扣後的餘額</strong>。兩層折扣各自對自己的空間 clamp，「應付金額不會為負」這個輸出契約才成立。多層減項的通用判讀：每一層的邊界要對「前面各層扣剩的餘額」算、不對原始值算。</p>
<h2 id="細節三擴充點先設計但不先實作">細節三：擴充點先設計、但不先實作</h2>
<p>未來的折扣規則（會員等級、促銷、優惠券、滿額減）還沒被後端定義，函式的註解預留了接入方式：規則成形時新增一個並列的 pure function（<code>cartDiscountView</code>），規則需要的 IO 資料同樣由 caller 拿好傳入，本函式在算完品項後呼叫它、把結果套進三個金額欄位——<strong>本函式與所有 UI 都不需要改</strong>。</p>
<p>這跟<a href="/blog/work-log/flutter_async_query_overdesign_oscillation/" data-link-title="同一個子系統膨脹兩次：異步查詢系統的過度設計震盪" data-link-desc="過度設計會復發、且兩輪的機制不同：設計期的膨脹來自想像的需求（別層已處理的重試、用不到的優先級佇列），迭代期的膨脹來自不刪的舊版本（三個實作並存、狀態多處追蹤）。偽需求的檢驗法是問「這個能力已經有別層在做嗎」。">過度設計震盪</a>裡「先做起來放」的偽需求處置形成正面對照：擴充點的設計成本是一段註解（接入位置、簽章形式、啟用點），實作成本是零——它把「未來怎麼接」的思考結果留下來、把「現在就寫」的浪費留在門外。同樣的態度也出現在整筆折扣的來源上：「商業邏輯尚未由後端定義，此處先以前端 model 自洽為主；未來後端開接口時 caller 改傳後端值即可」。</p>
<h2 id="判讀徵兆">判讀徵兆</h2>
<ul>
<li>同一段「從資料算出畫面要的數字」邏輯散在多個 widget / controller 裡——抽 pure function 的時機，重複第二次就該動手</li>
<li>領域計算的測試裡出現 repository mock——計算跟 IO 黏在一起了，把資料取得推給 caller、測試立刻減重</li>
<li>合併或對帳結果「偶爾多扣 / 少一筆」——查 key 的維度是否少於 domain 對「同一個」的定義</li>
<li>多層折扣 / 費用的邊界檢查全部對原始總額算——疊加後可能穿底，逐層對餘額 clamp</li>
</ul>
<h2 id="相關閱讀">相關閱讀</h2>
<ul>
<li>同專案的資料語意背景：<a href="/blog/work-log/pos_table_cart_lifecycle_decoupling/" data-link-title="桌子跟購物車是兩個聚合 — 從「提前結帳」推導生命週期解耦" data-link-desc="兩個業務資源該綁死成一對一、還是解耦成獨立生命週期加綁定關係——判準是有沒有業務操作需要其中一方獨立存活。以 POS 的提前結帳、純佔桌、外賣單推導桌位與購物車的聚合邊界，含組合空間大於業務空間時的非法組合封鎖。">桌子跟購物車是兩個聚合</a>——提前結帳的生命週期設計是本文「份數扣減」需求的來源；<a href="/blog/work-log/dart_pos_item_four_lifecycle_models/" data-link-title="同一個品項、四個 model — value object 什麼時候該升級成 entity" data-link-desc="同一個業務概念要不要拆成多個 model、value object 什麼時候該升級成 entity——判準是操作需不需要 identity-based 回寫。以 POS 品項從點選、掛單、結算到歷史訂單的四階段模型為例，含 snapshot 與 live reference 的凍結時機。">同一個品項、四個 model</a>——合併產出的 <code>OrderedCartItem</code> 與 <code>sourceDetailIds</code> 的身份設計</li>
<li>概念地基：<a href="/blog/ddd/" data-link-title="DDD 領域驅動設計指南" data-link-desc="領域模型的理論與判準層：一袋欄位還是領域模型、什麼時候值得建 entity、不變式該落在哪一層強制、狀態轉換怎麼留下稽核軌跡、建構路徑怎麼設計。語言無關，實作限制路由到各語言模組。">DDD 領域驅動設計指南</a>——領域計算的歸屬、以及「從操作推導領域」（提前結帳這個操作逼出了整個 view 函式）</li>
</ul>
]]></content:encoded></item></channel></rss>