訊息專家三部曲(二) 例子篇 | Information Expert's Example
“ I have not failed. I've just found 10,000 ways that won't work. ”
複習前篇
在前篇中 ,了解訊息專家是一個怎樣的模式,它具體要解決的是如何分配職責的情境,並列出四個步驟去進行職責的釐清以及分配。 同時也提供一個簡單的用例分析去進行練習。本篇會先呈現反面例子,再透過訊息專家,呈現如何分配職責。
主要參與者 : 顧客
前置條件
顧客已經選擇了一些商品並添加到購物車中
系統中已經有這些商品的價格信息
顧客開始結賬流程
主要流程
1. 計算每件訂單項目的小計(商品單價 * 數量)
2. 將所有小計相加,得出訂單總價
3. 顯示計算出的總價給顧客
後置條件
訂單的總價被正確計算並顯示給顧客
反面例子與正面例子
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 這個物件,增加了系統中不必要的耦合性。
total += item.GetProduct().GetPrice() * item.GetQuantity();
試想一下,如果這時 Product 回傳的不是 double,而是因為要達成符合多種幣別而回傳的 Money 物件,那勢必會造成要修改 OrderCalculator 這個類別。
之後新增折扣、驗證訂單等等的職責,職責是否又到 OrderCalculator 類別裡了, OrderCalculator 勢必會變成一個大雜燴。 就算不是,那又要憑空創造一個不符合任何現實概念的類別,增加維護者與開發者之間的認知鴻溝了。
如何修正?
“ An object is a thing that has identity, state, and behavior. ”
依照資訊專家,可以進行以下步驟的解析
- 陳述職責 : 計算訂單總價
- 概括該職責所需訊息 : 訂單總價、訂單項目金額、商品數量、商品金額。
- 列出所需資料的提供者(物件) : 訂單項目、商品、訂單
- 判斷職責的歸屬
- 訂單總價 : 訂單
- 小計 : 訂單項目
- 商品數量 : 訂單項目
- 商品金額 : 商品
根據資訊專家,我們可以很自然的歸屬職責,畢竟訂單自己就是提供訂單總價最直覺的選項。修改後的程式碼如下所示
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 容易膨脹的原因,因此,我們使用訊息專家,將使用與實現分離。這種使用與實現分離,也是其他設計原則所關注的。