架構面試題 #4 - 抽象化設計;折扣規則的設計機制 (06/25 補完)

架構面試題 #4 - 抽象化設計;折扣規則的設計機制 (06/25 補完)

摘要提示

  • 折扣規則難題: 零售折扣型態繁多且疊加互斥規則複雜,僅用「歸納法」難以因應未知變化。
  • 抽象化核心: 隱藏細節、提取重點,讓主流程只依賴介面,細節變更不影響主系統。
  • 介面與封裝: 折扣以 RuleBase 抽象類別界定介面,多型讓不同規則以一致方式運作。
  • OOP 關聯: 封裝、繼承、多型相輔相成;抽象化做對,OOP 三權杖自然好用。
  • 結帳管線化: 購物車只需累計原價、收集各規則回傳之折扣、相減得總價,流程單純。
  • 標籤驅動: 用商品 Tags 表示規則適用範圍,鬆綁資料與規則的耦合,利於擴充維運。
  • 規則擴充: 新規則只需繼承 RuleBase 並加入清單,無須修改 POS 主程式。
  • 配對與跨區: 「飲料+鮮食」搭配以資料驅動(表格或標籤)與排序策略求對消費者最優。
  • 折扣排除: 將互斥標準「標籤化」或「旗標化」,由框架統一處理規則間交互影響。
  • 實務反思: 架構與重構是同一件事不同階段;抽象化可顯著降低複雜度並提升可維護性。

全文重點

本文以零售電商的「折扣機制」為題,展示如何用抽象化思維與 OOP 設計,化繁為簡地處理高度變動與複雜交互的折扣規則。作者先指出僅靠「歸納法」把已知規則參數化,難以面對未知變化與混搭情境(如第二件折扣、同商品加價、多品類配對、跨區搭配等),很容易讓主流程與大量 flags 綁死,造成 UX 與維護困境。

抽象化的定義在於隱藏細節、提取重點:主流程專注於「我買了什麼」和「折多少」,而不管「怎麼算」。於是定義折扣規則的抽象介面 RuleBase(Process 接收購物上下文,回傳若干 Discount),並以多型承接各種規則的差異;購物流程(POS)則固定為:計原價、逐規則蒐集折扣、列印明細、總價=原價-折扣總和。此結果讓主系統不再受制於規則細節,更便於擴充。

資料建模方面,Product 引入 Tags 以貼標籤方式宣告適用規則範圍(如#衛生紙、#熱銷飲品、#同商品加購優惠、#超值配飲料59),化解分類/勾選清單的維運痛點;折扣實作時,Rule 只需根據標籤篩選、排序與配對(例如以消費者最有利的價格排序),產生 Discount 即可。作者示範了多個規則:滿件現折、第二件 N 折、同品第二件加價購、任 N 件 Y 折、滿額現折,以及便利商店常見的「飲料+鮮食」配對(含 39/49/59 與跨區規則),使用資料驅動的表格或多標籤法,輕鬆覆蓋變化。

對於更進階的「規則互斥/排除」,作者主張由框架統一處理:引入 ExclusiveTag 或 AbandonDiscount 旗標,在 POS 套用某規則後給商品加上互斥標籤,下一規則透過 CartContext 過濾帶有互斥標籤的商品,達成同群互斥又保留跨群可疊加的彈性。這種「以資料記錄狀態、以框架標準化交互」的方式,避免把互斥判斷硬塞入個別規則,減少人為協作風險。

文章後段分享讀者 PR 多種作法:有人以配對表與 Queue 管理候選,亦有人將跨區折扣於規則清單展開,還有以 Proxy(複合規則)方式協調兩規則先後與互斥。作者逐一點評其設計選擇、可擴充性與隱性風險(如狀態重用、介面過度限制、無狀態試算的必要性),強調「標準化 vs 彈性」需取得平衡。

總結而言,抽象化是把正確的介面邊界切出來:讓主流程穩定、規則可插拔、資料以標籤與旗標傳遞語義;配合 OOP 三權杖(封裝、繼承、多型),即可用極少且易懂的程式碼,處理看似無窮盡的折扣變化。此思考同樣適用於 API 契約、微服務邊界與大型系統重構:先想清楚介面與責任,再談工具與最佳化,才能真正降低複雜度、提升可維護性與可演進性。

段落重點

問題: 折扣機制到底有多難搞?

零售折扣型態繁多(滿件現折、第二件折扣、同品加價購、買一送一、品類配對、跨區搭配),且常出現規則混用、互斥與會員差異等複雜性。以「歸納法」把現有規則參數化雖能快速上線,但一遇未知或混搭場景就爆炸,還會引入許多技術性 flags 影響 UX 與維運。作者以實際 DM 與超商活動為例,凸顯必須用更高層次的抽象化來處理,否則主流程與規則實作緊耦合、難以擴充與維護。

抽象化: 隱藏細節、提取重點

抽象化的目標是讓主系統只看重點,不被細節牽制;當重點被正確提取,細節自動被隱藏與隔離,後續變更也不影響主流程。重點與細節的分界就是介面(interface)。折扣問題的重點是「購物車有哪些商品能折多少」,計算細節無需主流程知道。對齊此目標定義介面,所有規則都在介面下各自實作,多型分發行為,主流程以固定步驟組合結果,即可在規則變化下保持穩定。

OOP 三大權杖: 封裝、繼承、多型

抽象化與封裝是一體兩面:封裝隱藏細節、抽象化表達概念;當多個型別有共性時,用繼承與一般化抽出基底,靠多型在同一介面下呈現不同行為。本文把折扣視為一族物件(RuleBase 衍生),以共同介面承接差異;主流程僅依賴基底型別操作,規則之間彼此獨立、可插拔,展現 OOP 強大擴展性。若抽象化不恰當,多型與封裝也會變得卡手,顯示邊界切錯。

思考: 將折扣機制抽象化

主流程只關心:未折總價、每個規則回報的折扣、最後金額。於是抽象出 RuleBase,規則接收購物上下文(商品清單、金額狀態等),回傳 Discount(來源規則、涉及商品、折抵金額)。為管理狀態與列印,導入 CartContext 封裝購物狀態;POS 持有已啟用規則清單並依序執行。如此,新增規則只需繼承 RuleBase,POS 無須修改,即可持續演進,達成與未知需求對齊的彈性。

案例實作: 模擬實際的訂單計算(步驟 1~2)

步驟1以最小模型起步:Product 含 SKU/Name/Price/Tags;CheckoutProcess 先計原價、後續扣折。步驟2定義抽象介面:RuleBase.Process(CartContext) 回傳 IEnumerable;Discount 記錄 Rule、Products、Amount。POS 依序執行規則、累積折扣、計算總價;CartContext 收納購買商品、已套用折扣與總價等狀態。此結構讓規則計算與主流程清晰分離。

案例實作: 實作第一與第二個規則(步驟 3~5)

示範兩個規則:任兩箱 88 折(以標籤篩選、排序、每兩件配一筆折扣)與滿千折百(檢查當前總價是否達門檻後回傳單筆折扣)。重構後 POS/CartContext/RuleBase 責任分明,規則以多型插入。新增規則只需:開發新的 RuleBase 衍生類別;在 LoadRules() 依優先順序加入清單。主流程不動,擴充自然生效,驗證抽象化邊界切對。

大亂鬥: 多規則綜合與標籤策略

引入多種常見規則並混用:滿件折現、第二件 N 折、同商品加價購、任 N 件 Y 折、飲料+鮮食配對與跨區、熱銷飲品兩箱折扣等。用 Tags 將指定商品集合化(如#衛生紙、#雞湯塊、#同商品加購優惠、#熱銷飲品、#超值配鮮食/飲料39/49/59),規則內再按邏輯配對與排序(以最高價先配,求對消費者最有利)。價差與折抵以 Discount 回報,收據可追溯每筆折扣與涉及商品,提升可解釋性。

進階挑戰(配對折扣與折扣排除)

配對折扣:用資料驅動(表格或多標籤)定義「飲料/鮮食 + 價位 = 套餐價」,程式按表格順序與價格排序配對,處理跨區情境可在資料側展開規則或在商品標籤同時標多區,兩種都不動主流程。折扣排除:由框架統一,用 ExclusiveTag/AbandonDiscount 等標記在商品上記錄已套用的互斥群組;CartContext 供「可見商品」查詢以過濾互斥,POS 在規則套用後為商品加上標記。這讓規則間交互由框架標準化,避免人治。

解決方案:來自讀者的 PR

多位讀者提供不同實作:有人用 SpecialOffer 管配對隊列與價格;有人在 LoadRules 外層展開跨區組合;亦有以 Proxy 模式(複合規則)協調兩規則先後與互斥。作者依抽象邊界、無狀態試算、狀態重置、安全性與彈性等面向評析利弊,提醒避免將過多限制塞進基底介面、或讓規則彼此過度耦合;更鼓勵以標籤與框架規範處理交互,兼顧標準化與彈性。

總結: 寫在最後(06/25 更新)

抽象化是將現實問題轉為清晰介面的關鍵能力;做對抽象化,OOP 的封裝/繼承/多型才能真正解耦複雜度。本文用少量、直觀的程式碼,示範了折扣系統如何以 RuleBase+CartContext+POS 的邊界切割,配合資料驅動(標籤、表格)與框架化的排除機制,因應未知變化、易於重構與擴充。工程實務上,架構與重構是一體兩面;優先想清楚介面邊界與責任,再談實作與最佳化,才能真正降低複雜度、提升維護性與演進性。

資訊整理

知識架構圖

  1. 前置知識:學習本主題前需要掌握什麼?
    • 物件導向三要素:封裝、繼承、多型
    • 介面與抽象類別的差異與運用(C# interface/abstract class)
    • 基本集合/序列處理(Linq、Enumerable、IEnumerable)
    • 軟體設計原則:抽象化、依賴反轉、開放封閉
    • 電商/POS 基本概念(購物車、商品、促銷與折扣)
  2. 核心概念:本文的 3-5 個核心概念及其關係
    • 抽象化:隱藏細節、提取重點;將「折扣規則的計算細節」從主流程隔離
    • 介面設計(RuleBase):用統一接口 Process(CartContext) 回傳 Discount[],實現多型
    • 上下文(CartContext):集中管理購物車狀態(已購商品、已套用折扣、總價)
    • 流程編排(POS):以固定管線(依序套用規則)計算總折扣與最終金額
    • 可擴充性:以新 Rule 擴充功能,不改動 POS/核心流程
  3. 技術依賴:相關技術之間的依賴關係
    • Product/Discount/RuleBase 為促銷引擎的領域模型
    • RuleBase 衍生類(各種折扣規則)依賴 CartContext 取得狀態
    • POS 依賴 RuleBase 列表(按優先順序)組合與執行
    • Tags/ExclusiveTag 等標記機制,作為跨規則的通用「資料協議」
    • JSON 載入商品(資料驅動),與程式內/外部配置結合(未來可外部化)
  4. 應用場景:適用於哪些實際場景?
    • 電商網站/超商 POS 的促銷引擎(滿件折、第二件 N 折、買 A 送 B、配對價)
    • 會員分級價、滿額折/券、跨品類組合價
    • 需要「快速上新活動」且「不動核心程式」的行銷場景
    • 微服務架構中獨立折扣服務(遵守 API/Contract,便於演進)

學習路徑建議

  1. 入門者路徑:零基礎如何開始?
    • 了解抽象化與封裝的意義(重點與細節的切分)
    • 熟悉 C# class/interface/abstract class、IEnumerable、Linq
    • 跑通最基礎範例:無折扣 → 單一折扣(如「任 2 件 88 折」)
  2. 進階者路徑:已有基礎如何深化?
    • 設計與實作 RuleBase、Discount、CartContext、POS 的協作
    • 新增多種規則:滿件固定折、第二件 N 折、同品加價多一件
    • 引入規則順序、最佳利益(排序/選擇)與排除(ExclusiveTag)
  3. 實戰路徑:如何應用到實際專案?
    • 將規則參數與對應 Tags 外部化(DB/設定檔),做到資料驅動
    • 建立規則優先權與互斥組群策略;制定標籤治理規範
    • 撰寫單元測試(邊界、疊加、互斥、最佳化配對)
    • 以微服務/模組化釋出折扣引擎,透過 API 與結帳系統整合

關鍵要點清單

  • 抽象化定義:隱藏細節、提取重點(讓主流程只面對統一接口) (優先級: 高)
  • RuleBase 介面:規範折扣規則以 Process(CartContext) → IEnumerable 回報結果 (優先級: 高)
  • POS 管線:依序執行規則、彙總 Discount、計算總價的固定流程 (優先級: 高)
  • CartContext:集中購物車狀態(PurchasedItems、AppliedDiscounts、TotalPrice) (優先級: 高)
  • Discount 模型:描述折扣來源、涉及商品與折抵金額,作為統一回報格式 (優先級: 高)
  • 商品 Tags:以標籤定義「指定商品/活動歸屬/分區」,支撐規則篩選 (優先級: 高)
  • 規則優先序:以順序或 Priority 控制疊加與互斥的先後影響 (優先級: 高)
  • 折扣排除機制:ExclusiveTag(或 Abandon 標記)標註已占用、後規則需避開 (優先級: 高)
  • 配對折扣:以飲料/鮮食分區與組合價表,排序(高價優先)求消費者最有利 (優先級: 中)
  • 資料驅動規則:將規則參數、標籤對應、跨區組合外部化(配置/DB) (優先級: 中)
  • 多型擴充:新增規則=新增類別+加入清單;不修改主流程 (優先級: 高)
  • 試算與無狀態思維:回傳 Discount 而非直接改全域狀態,便於比較/回滾 (優先級: 中)
  • 標準化 vs 彈性:避免過度限制(僅靠單一 Tag/單一旗標)而失去通用性 (優先級: 中)
  • 測試覆蓋:單元測試涵蓋疊加、互斥、跨區配對、落單、極端數量 (優先級: 中)
  • 微服務整合:以清晰合約(API/DTO)封裝引擎,支援獨立演進與部署 (優先級: 低)





Facebook Pages

AI Synthesis Contents

Edit Post (Pull Request)

Post Directory