訊息專家三部曲(二) 例子篇 | Information Expert's Example

2024-08-05
GRASP

“ I have not failed. I've just found 10,000 ways that won't work. ”

― Thomas Edison

複習前篇

前篇中 ,了解訊息專家是一個怎樣的模式,它具體要解決的是如何分配職責的情境,並列出四個步驟去進行職責的釐清以及分配。 同時也提供一個簡單的用例分析去進行練習。本篇會先呈現反面例子,再透過訊息專家,呈現如何分配職責。

用例 : 計算訂單總價

主要參與者 : 顧客

前置條件
顧客已經選擇了一些商品並添加到購物車中
系統中已經有這些商品的價格信息
顧客開始結賬流程

主要流程
1. 計算每件訂單項目的小計(商品單價 * 數量)
2. 將所有小計相加,得出訂單總價
3. 顯示計算出的總價給顧客

後置條件
訂單的總價被正確計算並顯示給顧客

反面例子與正面例子

csharp
📝copy
                                
class OrderCalculator
{
    public double CalculateTotal(Order order)
    {
        double total = 0;
        foreach (OrderItem item in order.GetItems())
        {
            total += item.GetProduct().GetPrice() * item.GetQuantity();
        }
        return total;
    }
}

class Order
{
    private List<OrderItem> items;

    public List<OrderItem> GetItems()
    {
        return items;
    }
}

class OrderItem
{
    private Product product;
    private int quantity;

    public Product GetProduct()
    {
        return product;
    }

    public int GetQuantity()
    {
        return quantity;
    }
}

class Product
{
    private double price;

    public double GetPrice()
    {
        return price;
    }
}                      
                                
                            

可以正確地執行,每個物件也沒有過多的職責,很完美。 如果看到這邊可以停下來,想想為什麼是反面例子? 真正透過訊息專家推導出的程式碼應該是怎樣?

為什麼是反面例子?

把職責分配給具有完成該職責所需訊息的那個類別,是訊息專家的精隨,但在上方的反面例子中,我們看到 Order 擁有所有關於訂單總價的訊息但我們另外分配職責到另一個類別。

雖然在後續的純虛構中,也可以看到這種操作,但該職責屬於商業邏輯,並且沒有違反其他 GRASP 的原則,因此不算一個好的決策。

而其中以下這段程式碼,會讓 Order 透過 OrderItem 耦合到 Product 這個物件,增加了系統中不必要的耦合性。

csharp
📝copy
                                
total += item.GetProduct().GetPrice() * item.GetQuantity();                
                                
                            

試想一下,如果這時 Product 回傳的不是 double,而是因為要達成符合多種幣別而回傳的 Money 物件,那勢必會造成要修改 OrderCalculator 這個類別。

之後新增折扣、驗證訂單等等的職責,職責是否又到 OrderCalculator 類別裡了, OrderCalculator 勢必會變成一個大雜燴。 就算不是,那又要憑空創造一個不符合任何現實概念的類別,增加維護者與開發者之間的認知鴻溝了。

如何修正?

“ An object is a thing that has identity, state, and behavior. ”

― Grady Booch

依照資訊專家,可以進行以下步驟的解析

  1. 陳述職責 : 計算訂單總價
  2. 概括該職責所需訊息 : 訂單總價、訂單項目金額、商品數量、商品金額。
  3. 列出所需資料的提供者(物件) : 訂單項目、商品、訂單
  4. 判斷職責的歸屬
    • 訂單總價 : 訂單
    • 小計 : 訂單項目
    • 商品數量 : 訂單項目
    • 商品金額 : 商品

根據資訊專家,我們可以很自然的歸屬職責,畢竟訂單自己就是提供訂單總價最直覺的選項。修改後的程式碼如下所示

csharp
📝copy
                               
class Order
{
    private List<OrderItem> items;

    public double CalculateTotal()
    {
         double total = 0;
        foreach (OrderItem item in items)
        {
            total += item.GetSubtotal();
        }
        return total;
    }
}
    
class OrderItem
{
    private Product product;
    private int quantity;
    
    public double GetSubtotal()
    {
        return product.GetPrice() * quantity;
    }
}
    
class Product
{
    private double price;

    public double GetPrice()
    {
        return price;
    }
}             
                                 
                            

相信經過修改後的程式碼,會有種各司其職的感覺,那為什麼呢 ? 關於上面提到反面例子之所以是反面例子的說法,有沒有對應的、解釋力更強的原則可以進行說明 ?

這就是最後一篇要講解的了。


作者心得

透過錯誤,我們能知道正確的邊界。對我來說,透過反面例子,更清晰的呈現了資訊專家的核心概念。雖然這個概念直觀,但卻不像呼吸那樣自然。 其中一部分原因我認為是使用職責實現職責的概念模糊。

狗會『叫』、鳥會『飛』這些例子,如果將『叫』視為職責,狗使用了『叫』,狗實現『叫』,都是十分直覺的,但只要涉及非動物的概念時,像是訂單會『計算總價』,這種描述就會非常割裂。 而通常感到割裂,就會把割裂的職責分配到使用職責的那個物件,或另外創造一個物件來承擔這職責

這也是為什麼 MVC 模式中的 Controller 容易膨脹的原因,因此,我們使用訊息專家,將使用與實現分離。這種使用與實現分離,也是其他設計原則所關注的。


參考資料

也可以看看以下文章