Blog

抽象化系統設計思維以促購引擎為例

zhiwei-lu.jpg
Zhi-Wei Lu2020/06/24


這個主題我們會和大家分享透過抽象化思維精心設計出的全新促購引擎來因應市場變化取得先機,其中會簡介系統架構與技術細節,以及新促購引擎帶來的效益與新的挑戰。

很高興在這邊和大家分享在電商技術中非常重要一環的促購機制,在 91APP 我們有著與以往完全不同的新促購機制,我們透過抽象化的思維精心設計出全新的促購引擎,這個優秀的系統設計思維可以讓系統未來的發展性與維護性帶來巨大的效益,對於瞬息萬變的市場變化,也有著良好的適應性,讓企業得以應變並取得先機,就讓我們一起來瞭解 91APP 的促購引擎吧!


0. 大綱

在這整個主題中我們將會細分為四個小節來向大家詳細說明,首先,我們會先介紹新促購機制的來龍去脈並說明新促購引擎的系統架構與技術細節;第二,我們會分享在使用進化後的促購引擎為我們帶來了什麼樣實際的效益;第三,我們也揭露在這個新促購機制完成後,我們所接受到那些新的挑戰;最後,我們會針對新促購機制的重點做個總結。


1. 新促購機制 ── 簡介新促購引擎

在這一小節我們會先介紹新促購機制的來龍去脈並說明新促購引擎的系統架構與技術細節。

1.1. 背景

我們先來說明一下什麼是促購。

促購是指商店向消費者傳遞有關產品的各種訊息,吸引消費者注意商店的產品、激發消費者的購買欲望,並促使其實現最終的購買行為。常用的促購方式有降價、打折、贈送等方式。商店可根據實際情況及市場、產品等因素選擇一種或多種促購方式的組合[1, 2]

Sales promotion 圖片來源

我們現在的促購活動種類非常多,而且在折扣方式上也是相當複雜且變化多端,這邊舉幾個常見促購活動的例子,例如:

  • 指定商品任選優惠價,像是:滿 2 件,合計 399 圓;
  • 現折:滿 1000 圓,全部折 100 圓;
  • 第 N 件折扣:滿 2 件,第 2 件 6 折;
  • 紅配綠:1 件紅標商品加 1 件綠標商品,合計 999 圓。

等等更多更複雜更多變的折扣方式,還有未來新型的促購活動。

1.2. 動機

身為全通路新零售解決方案平臺,我們希望能夠使用更多變更多重的折扣方式,以搭配商店的促購活動帶動業績,讓商店與消費者達到雙贏的目標。同時我們也希望簡化購物車計算流程,抽象化折扣計算邏輯以便日後的維運,並對未來市場上促購活動的變化有著良好的適應性,使得我們能夠快速開發新的折扣方式為商店取得先機。

1.3. 目標

我們對於新的促購機制訂下兩個目標:第一,我們希望統一運算規則以解決商業邏輯不一致,難以驗證溝通等等的問題;第二,打造可以擴充的計算引擎,將折扣規則標準化,加速開發與上線的時程。接下來我們將朝著這兩個目標一步一步實現我們的新促購機制。

1.4. 挑戰

要實現我們的目標會先面臨到幾個統整折扣規則的主要挑戰,就像我們先前提到的促購活動範例一樣,首先,遇到的挑戰是各個折扣規則皆大不相同,以及,各個折扣規則適用的商品範圍也是都完全不同,還有,各種折扣規則的組合順序,例如:要考慮是否先計算紅配綠再計算現折等等的組合順序。而這些困難的挑戰都是一個優秀的促購機制必須面對的議題。

1.5. 解決方案

在分享解決方案前,我們先來快速說明在物件導向程式設計中非常重要的抽象化,藉由 91APP 首席架構師 Andrew 所分享文章中對抽象化的解釋,我們可以知道物件導向程式設計中的抽象化是:「提取重點」與「隱藏細節」。而提取重點的目的是為了讓主系統只依照被提取的重點設計流程;隱藏細節的目的則是讓跟重點無關的細節不會影響主系統的設計;即使日後改變細節也不會影響到主系統的運作[3]。這樣設計的好處我們稍後就可以清楚的看到了!

  • 什麼是抽象化:
    • 提取重點:讓主系統只依照被提取的重點設計流程;
    • 隱藏細節:讓跟重點無關的細節不會影響主系統的設計[3]

為了克服挑戰達成目標,透過物件導向程式設計的抽象化來設計我們的系統,首先,必須要先知道我們要解決的問題須要提取什麼樣的重點,以及隱藏什麼樣的細節。從購物車結帳流程的角度來看,購物車只需要知道那些商品符合折扣條件,以及符合折扣後的折抵金額就可繼續進行結帳流程。所以,我們提取最終的折扣金額,以及隱藏折扣的計算規則。這個良好的抽象化結果可以符合我們目標,在購物車結帳流程中無論未來有任何新的折扣規則,皆可計算出最終的折扣金額順利地進行結帳。

我們從這張概念圖來說明,在購物車結帳時不需要知道折扣是如何計算,也不需知道折扣的規則是什麼,僅需要關注在計算後的折扣金額即可,所以我們將購物車裡的商品資訊傳給促購引擎,由引擎負責計算並將計算完的折扣金額回傳給購物車繼續進行結帳。換句話說無論未來折扣規則如何變化購物車的結帳流程還是不變,一樣將商品傳入取得折扣金額即可。

  • 將結帳流程抽象化:

將結帳流程抽象化

接下來,我們會透過範例程式碼說明我們促購引擎的實作概念。在隱藏折扣計算的細節後,購物車結帳流程需要處理的項目只有:第一,描述商品資訊(包含品名、標籤及售價);第二,呼叫引擎進行的折扣計算;最後,結帳的收據明細顯示。

我們先從定義系統架構開始,在 Main 方法中我們模擬購物車結帳的流程,首先,初始化購物車 Context,並將購買的商品加入到購物車中,再初始化 POS 物件,也將啟用的折扣規則載入;接著,就可以呼叫結帳方法 CheckoutProcess 進行結帳,最後將計算結果列印出來,這樣大致就完成了整個結帳流程的系統架構,這邊也就呼應上述購物車結帳要處理的項目囉!

private static void Main(string[] args)
{
    // 初始化購物車 Context 與 POS 物件。
    var cart = new CartContext();
    var pos = new Pos();

    // 1. 購物車加入購買商品。
    cart.PurchasedItems.AddRange(LoadProducts());
    // POS 加入啟用的折扣規則。
    pos.ActivatedRules.AddRange(LoadRules());

    // 2. 對購物車 Context 進行結帳。
    pos.CheckoutProcess(cart);
    // 3. 列印收據。
    PrintReceipt(cart);
}

接著,我們來看一下 POSCheckoutProcess 結帳方法細節,在這個方法的重點只需要計算商品的總價,以及逐一呼叫折扣規則的 Process 方法取得折扣資訊即可,完全不需要知道折扣規則的邏輯與細節就可以取得折扣資訊繼續進行結帳;這裡我們就是透過抽象化折扣規則與多型的應用達到的效果。那麼,抽象化的折扣規則是什麼呢?

public bool CheckoutProcess(CartContext cart)
{
    // Reset Cart.
    cart.AppliedDiscounts.Clear();
    // 計算總價。
    cart.TotalPrice = cart.PurchasedItems.Select(p => p.Price).Sum();

    // 計算折扣。
    foreach (var discounts in this.ActivatedRules.Select(
        rule => rule.Process(cart).ToArray()))
    {
        cart.AppliedDiscounts.AddRange(discounts);
        cart.TotalPrice -= discounts.Select(d => d.Amount).Sum();
    }

    return true;
}

那我們來看一下折扣規則的抽象化介面是如何定義,其實非常簡單,除了折扣規則的基本資訊像是:IDNameNote 之外,就只有一個要實作的抽象方法 Process 而已。所有的折扣規則只需要繼承這個 RuleBase 類別後,在覆寫的 Process 方法中實作自己的折扣邏輯就可讓前一頁提到的 CheckoutProcess 結帳方法呼叫並進行結帳。接下來,我們就一起來實作折扣規則吧!

public abstract class RuleBase
{
    public int Id { get; set; }

    public string Name { get; set; }

    public string Note { get; set; }

    public abstract IEnumerable<Discount> Process(CartContext cart);
}

我們嘗試實作第一個折扣規則:任兩箱結帳 88 折。就像剛才提到的一樣,這個折扣規則只需要繼承 RuleBase 抽象類別後,實作自己的 Process 方法即可,我們粗略的看一下這個方法如何實作;也是很簡單,只要逐一檢查購買的商品是否有符合折扣條件,若符合就回傳計算後的折扣資訊即可。那我們還有其他的折扣規則要怎麼做呢?

public class BuyMoreBoxesDiscountRule : RuleBase
{
    public override IEnumerable<Discount> Process(CartContext cart)
    {
        var matchedProducts = new List<Product>();

        foreach (var p in cart.PurchasedItems)
        {
            matchedProducts.Add(p);

            if (matchedProducts.Count != this._boxCount)
            {
                continue;
            }

            // 符合折扣
            yield return new Discount
            {
                Amount = matchedProducts.Select(product => product.Price)
                    .Sum() * this._percentOff / 100,
                Products = matchedProducts.ToArray(),
                Rule = this,
            };

            matchedProducts.Clear();
        }
    }
}

我們就再來擴充第二個折扣規則:消費滿 1000 折抵 100,就跟上一個折扣規則一樣,繼承 RuleBase 類別實作 Process 方法,在 Process 方法中判斷符合條件就回傳折扣資訊。像這樣,我們讓每個折扣規則只需要關注自己的折扣邏輯,不用知道結帳流程是如何處理,反過來說結帳流程也不需要知道折扣的規則,大幅降低了雙方的複雜度與耦合度,未來在開發新的折扣規則也就能更加簡單、更加快速!

public class TotalPriceDiscountRule : RuleBase
{
    public override IEnumerable<Discount> Process(CartContext cart)
     {
        if (cart.TotalPrice > this._minDiscountPrice)
        {
            yield return new Discount
            {
                Amount = this._discountAmount,
                Rule = this,
                Products = cart.PurchasedItems.ToArray()
            };
        }
    }
}

我們這邊再針對範例程式碼和大家分享幾個關鍵點,首先是折扣規則的抽象化,從上述的程式碼可以看到,我們將折扣規則抽象化為 RuleBase 的抽象類別,而實作的折扣規則都只要關注本身的折扣邏輯,其餘的結帳、計算最終金額與顯示收據等則不在其中。

另一個關鍵點是多型的應用,在結帳的 CheckoutProcess 方法中,由於已經將折扣規則抽象化所以只需要:第一,計算原價;再來,將商品傳給每個折扣規則進行處理,取得折扣資訊;最後,回傳最終結帳金額,就完成了。而在呼叫折扣規則都只透過 RuleBase 定義的 Process 方法存取,而剩下的折扣計算,透過物件導向程式設計的多型機制,就會幫我們重新導向到每個衍生類別自己定義的邏輯進行處理。

經過開發前期仔細的思考與分析,透過抽象化的方法設計出新促購機制的解決方案,符合我們的預期並達到了目標,統一了運算規則,抽象化結帳流程讓折扣的運算皆交由促購引擎計算並取的最終的折購金額;也打造可擴充的計算引擎,抽象化折扣規則讓任何依照合約實作的折扣規則皆可擴充至促購引擎


2. 帶來的效益

在這一小節我們會分享在使用進化後的促購引擎為我們帶來了什麼樣的實際效益。

在新促購機制這個解決方案完成後,我們感受到下列三個明顯的效益:首先是,可擴充折扣規則;第二,加快開發速度;最後是,元件化促購引擎。接下來我們和大家分享每一個項目的效果與利益。

接下來我們來說明一下第一項效益,可擴充的折扣規則:透過抽象化折扣規則,實作相同合約的折扣規則皆可擴充至促購引擎,讓結帳流程可以呼叫並取得折扣資訊,使得我們有能力新增各類型或未知的折扣規則,讓商店可以更加靈活運用各種折扣活動,來滿足多變的市場需求。在今年第一季為止我們已經擴充了 8 種類型的折扣規則,例如:第 N 件打折、任選優惠價,以及滿額折現等等種類的折扣規則,而在今年第一季受惠的商店就已經達到約 740 家,我們目前也積極的新增其他折扣類型!

第二項帶來的效益是加快開發速度,在抽象化促購引擎將關注點分離後,新增折扣規則時僅須著重於該折扣規則的開發即可,相對的能夠加快我們的開發速度;而我們的開發速度也大幅度的提升約 2 至 4 倍,就像現今只需要約 2 週的開發時間,在過去則需 4 至 8 週的開發時間。這也意味著我們能夠更快的取得市場先機,為商店帶來更多的利益。

最後還有一項效益是促購引擎的元件化,我們將計算折扣的機制抽離購物車結帳流程後,元件化的促購引擎更容易被測試與驗證,這對於我們在開發與測試階段都有莫大的幫助,同時也更容易開發試算或模擬工具,讓設計促購活動時能離線試算,而更快速的知道折扣結果,可以告訴商店折扣後有沒有可能低於成本,也就能更有效率的與客戶溝通。


3. 新的挑戰

在這一小節我們也揭露在這個新促購機制完成後,我們所接受到那些新的挑戰。

在新促購機制完成後,我們接下來還有新的挑戰,首先,必須要考慮的是新舊促購機制的並存,在大部分都還是原有的促購機制下我們如何整合新的促購機制,既不能互相影響又要計算正確的折扣;以及有順序性的折扣規則,例如指定要先計算第 N 件折扣後再計算紅配綠折扣,類似像這樣的折扣順序;還有,折扣排除,在符合任選優惠後就不再適用第 N 件折扣;擇優,在眾多符合條件的折扣中選擇最優惠的折扣;最後,除了折扣活動外還有折價券這類的促購也要納入計算等等。當然,這些挑戰我們都會一一克服讓新促購引擎能更趨於完善。


4. 總結

最後一小節,我們來針對新促購機制的重點做個總結。

藉由這個新促購機制的案例來看,在設計系統架構時,適當的運用物件導向程式設計概念,會讓我們的系統能更容易適應變化;而對系統進行良好的抽象化,也可以讓系統對於未來的擴充性與維護性都大幅上升;最後,確實在經過仔細思考分析後所設計的系統,能夠比事後再來最佳化更能夠減少浪費發揮效益,也能讓系統保持整潔、好維護、好理解以及速度快等等,這也是所有軟體工程師努力探求的極致!


5. 附錄

最後,這邊附上簡報中範例程式碼 GitHub 儲存庫的連結,有興趣的朋友歡迎來取用;同時也附上這次主題中的參考資料供大家延伸閱讀。

5.1. 範例程式碼

5.2. 參考資料

  1. 促銷 - MBA 智庫百科
  2. 銷售促進-華人百科
  3. 架構面試題 #4 - 抽象化思考;折扣規則的設計機制 — 安德魯的部落格
  4. 抽象化 (計算機科學) - 維基百科,自由的百科全書
© 2021 91APP, Inc. 版權所有
* Version v0.9.6 * 2021/05/29 14:44