標籤:
第20章 咖啡的啟示
這個例子對於教學有很多好處。它短小、易於理解並且展示了如何應用物件導向設計原則去管理依賴和分類關注點。但從另一方面來說,它的短小也意味著這種分離帶來的好處可能抵不過其成本。就當做一個設計思路來看吧。
20.1 Mark IV型專用咖啡機
20.1.1 規格說明書
Mark IV型專用咖啡機一次可以產出12杯咖啡。使用者把過濾器放置在支架上,在其中裝入研磨好的咖啡,然後把支架推入其容器中。接著,使用者向濾水器中倒入12杯水並按下沖煮(Brew)按鈕。水一直加熱到沸騰。不斷產生的水蒸氣壓力使水灑在咖啡粉末上,形成水滴通過過濾器流入到咖啡壺中。咖啡壺由一個保溫盤進行長期保溫,僅當壺中有咖啡時,保溫盤才進行工作。如果在水還在向咖啡粉噴洒時從保溫盤上拿走咖啡壺,水流就會停止,這樣煮好的咖啡就不會濺在保溫盤上。以下是需要監控的硬體裝置。
- 加熱器的加熱元件。可以開啟和關閉。
- 加熱盤的加熱元件。可以開啟和關閉。
- 保溫盤感應器。它有3個狀態:warmerEmpty、potEmpty和potNotEmpty。
- 加熱器感應器,用來判斷是否有水。它有兩個狀態:boilerEmpty、boilerNotEmpty。
- 沖煮按鈕。這個瞬時按鈕啟動沖煮流程。它有一個指示燈,當沖煮流程結束時亮,表示咖啡已經煮好。
- 減壓閥門,在開啟時可以降低加熱器中的壓力。壓力降低會阻止水流向過濾器。該閥門可以開啟和關閉。
Mark IV型專用咖啡機的硬體已經設計完成,硬體工程師為我們提供了低層的API,如下:
namespace CoffeeMaker{ public enum WarmerPlateStatus { WARMER_EMPTY, POT_EMPTY, POT_NOT_EMPTY }; public enum BoilerStatus { EMPTY, NOT_EMPTY }; public enum BrewButtonStatus { PUSHED, NOT_PUSHED }; public enum BoilerState { ON, OFF }; public enum WarmerState { ON, OFF }; public enum IndicatorState { ON, OFF }; public enum ReliefValveState { OPEN, CLOSED }; public interface CoffeeMakerAPI { /* * This function returns the status of the warmer-plate * sensor. This sensor detects the presence of the pot * and whether it has coffee in it. */ WarmerPlateStatus GetWarmerPlateStatus(); /* * This function returns the status of the boiler switch. * The boiler switch is a float switch that detects if * there is more than 1/2 cup of water in the boiler. */ BoilerStatus GetBoilerStatus(); /* * This function returns the status of the brew button. * The brew button is a momentary switch that remembers * its state. Each call to this function returns the * remembered state and then resets that state to * NOT_PUSHED. * * Thus, even if this function is polled at a very slow * rate, it will still detect when the brew button is * pushed. */ BrewButtonStatus GetBrewButtonStatus(); /* * This function turns the heating element in the boiler * on or off. */ void SetBoilerState(BoilerState s); /* * This function turns the heating element in the warmer * plate on or off. */ void SetWarmerState(WarmerState s); /* * This function turns the indicator light on or off. * The indicator light should be turned on at the end * of the brewing cycle. It should be turned off when * the user presses the brew button. */ void SetIndicatorState(IndicatorState s); /* * This function opens and closes the pressure-relief * valve. When this valve is closed, steam pressure in * the boiler will force hot water to spray out over * the coffee filter. When the valve is open, the steam * in the boiler escapes into the environment, and the * water in the boiler will not spray out over the filter. */ void SetReliefValveState(ReliefValveState s); }}
我們在為一個簡單的嵌入式即時系統設計軟體。我期望可以給出一組類圖、順序圖和狀態圖。
20.1.2 常見的醜陋方案
展示了最常見的醜陋方案:
對於初學者來說,很難認出這個設計是多麼醜陋。在這幅圖中隱藏著一些非常嚴重的錯誤。其中有許多隻有你開始編寫針對這個設計的代碼時才會注意到,那時你就會發現寫出的代碼多麼荒謬。
先來看看這幅UML圖建立方式的一些問題。
缺少方法
當設計者建立出沒有方法的視圖時,他們也許不是根據行為對軟體進行劃分的。不基於行為的劃分基本上都有嚴重錯誤。正是系統的行為為我們提供了第一個關於如何劃分系統的線索。
水蒸氣類
如果考慮一下Light類應該具有的方法,就會發現這個設計所劃分是多麼糟糕。顯然,Light對象應該能夠被開啟或者關掉。因此,我們會把On()和Off()方法放進Light類中。這些函數實現出來會是什麼樣子呢?如下:
public class Light { public void On() { CoffeeMaker.api.SetIndicatorState(IndicatorState.ON); } public void Off() { CoffeeMaker.api.SetIndicatorState(IndicatorState.OFF); } }
Light類有幾個奇怪的地方。首先,他沒有任何成員變數。有些不尋常,因為對象通常會具有某種要操作的狀態。此外,On()和Off()方法只是簡單地把工作委託給CoffeeMakerAPI的SetIndicatorState方法。顯然,Light類只是一個調用轉換器,沒有做任何有用的事情。
Button類、Boiler類和WarmerPlate類具有同樣的問題。它們都只是把一種調用格式轉換成另外一種格式的適配器。事實上,完全可以把它們從設計中去掉而不引起CoffeeMaker類的邏輯發生任何變化。CofferMaker類完全可以直接調用CoffeeMakerAPI而不是使用這些適配器。
通過研究這些類的方法和代碼,我們把那些在圖中佔有重要位置,降格為沒有多少存在必要的純粹的預留位置。因此,我們稱它們為水蒸氣類。
20.1.3 虛構的抽象
系統中所有類都沒有使用Sensor和Heater類。它們最多具有一些抽象的方法。比如Heater介面。一個僅僅含有抽象方法並且不具有任何使用者的類,完全是一個無用類。
public interface Heater { void TurnOn(); void TurnOff(); }
起初看了,好像確實有很多具有意義功能的類。但是當我們開始編寫實現浙西類的代碼時,就會發現其中只有一個CofferMaker類才具有一些有意義的行為,其餘所有的類要麼是虛構的抽象,要麼是水蒸氣類。
20.1.4 改進方案
解決這個問題(乃至任何問題)的技巧是:退一步,把問題的本質和細節分離。忘掉加熱器、閥門、感應器等所有細小的細節,集中關注與根本的問題。什麼是根本問題?就是如何煮咖啡。
如何煮咖啡?最簡單、最常見的方法是把熱水倒在研磨好的咖啡上,並把沖好的咖啡液體收集在某種器皿中。熱水從哪來?從HotWaterSource類。把咖啡存放在什麼地方?存放在ContainmentVessel中。
考慮一下Mark IV 的組件,就可以想象得到加熱器、閥門及加熱感應器在充當HotWaterSource角色。HotWaterSource負責吧水加熱並噴洒在研磨好的咖啡上,形成溶液流入ContainmentVessel中。我們還可以想象得到保溫盤及其感應器在充當ContainmentVessel的角色。他負責保持存放咖啡的溫度,並讓我們知道容器中是否有咖啡。
如何使用UML來描述上面討論?展示了一種可能的做法。 HotWaterSource和ContainmentVessel都是類,通過咖啡流關聯起來。
這種關聯是初學者常犯的一個錯誤。該關聯是基於問題中的一些物理關聯而非軟體控制行為作出的。咖啡從HotWaterSource流入ContainmentVessel和這兩個類之間的關聯完全無關。
如果通向器皿的熱水流的開始和停止是有ContainmentVessel通知HotWaterSource進行的,會怎樣呢?如下展示,注意,ContainmentVessel給HotWaterSource發送了start訊息。這意味著關聯關係是反方向的。ContainmentVessel依賴於HotWaterSource。
關聯是對象之間訊息發送的路徑。關聯和物理實體的流向沒有任何關係。
現在還沒有提供任何方法使得人可以和我們的系統進行交換。系統必須能夠向主人報告自己的工作狀態。我們向咖啡機模型中增加了一個UserInterface類。
我們來觀察幾個用例,看看是否可以找出這些類的行為。
用例1:使用者按下沖煮按鈕
當HotWaterSource和ContainmentVessel都準備好,UserInterface對象就應該向HotWaterSource發送start訊息,HotWaterSource就開始工作。
用例2:接收器皿沒有準備好
當接收器皿沒有準備好,ContainmentVessel通知HotWaterSource停止傳送熱水,當準備好後,再通知HotWaterSource再次開啟熱水流,熱水流的終止和恢複如下:
用例3:沖煮完成
HotWaterSource和ContainmentVessel都可以發送Done訊息。
用例4:咖啡喝完了
當沖煮結束並且一個空咖啡壺被放在保溫盤上時,Mark IV 就會關掉指示燈。
根據這幅圖,我們可以畫出一幅具有相同關聯關係的類圖。如下:
20.1.5 實現抽象模型
我們建立的3個類都不能知道關於Mark IV的任何訊息。這就是依賴倒置原則(DIP)。我們不允許系統中高層的咖啡製作策略依賴於低層的實現。
使用者按下沖煮按鈕
UserInterface如何知道沖煮按鈕被按下了呢?它必須要調用CoffeeMakerAPI.GeeBrewButtonStatus()函數。我們決定UserInterface類是不能知道CoffeeMakerAPI的。根據DIP,這個調用放在UserInterface的衍生類別中。
代碼如下:
public class M4UserInterface : UserInterface { private void CheckButton() { BrewButtonStatus status = CoffeeMaker.api.GetBrewButtonStatus(); if (status == BrewButtonStatus.PUSHED) { StartBrewing(); } } } public class UserInterface { private HotWaterSource hws; private ContainmentVessel cv; public void Done() { } public void Complete() { } protected void StartBrewing() { if (hws.IsReady() && cv.IsReady()) { hws.Start(); cv.Start(); } } }
為什麼要建立一個受保護的StartBrewing()方法呢?維護不再M4UserInterface中直接調用Start()函數呢?原因很簡單,但是很重要。IsReady()測試以及隨後對HotWaterSource和ContainmentVessel的start()方法的調用都是高層的策略,都應歸屬於UserInterface類。
實現IsReady()方法
public class M4HotWaterSource : HotWaterSource { public override bool IsReady() { BoilerStatus status = CoffeeMaker.api.GetBoilerStatus(); return status == BoilerStatus.NOT_EMPTY; } } public class M4ContainmentVessel : ContainmentVessel { public override bool IsReady() { WarmerPlateStatus status = CoffeeMaker.api.GetWarmerPlateStatus(); return status == WarmerPlateStatus.POT_EMPTY; } }
實現Start()方法
HotWaterSource的Start()方法只是一個抽象方法,M4HotWaterSource會實現該方法調用CoffeeMakerAPI中關閉閥門以及開啟加熱器的函數。在編寫這些函數過程中,我開始不停地寫一些類似CoffeeMaker.api.XXX這樣的結構感到厭煩,因此我就同時也做了一些重構。
public class M4HotWaterSource : HotWaterSource { private CoffeeMakerAPI api; public M4HotWaterSource(CoffeeMakerAPI api) { this.api = api; } public override bool IsReady() { BoilerStatus status = api.GetBoilerStatus(); return status == BoilerStatus.NOT_EMPTY; } public override void Start() { api.SetReliefValveState(ReliefValveState.CLOSED); api.SetBoilerState(BoilerState.ON); } } public class M4ContainmentVessel : ContainmentVessel { private CoffeeMakerAPI api; private bool isBrewing = false; public M4ContainmentVessel(CoffeeMakerAPI api) { this.api = api; } public override bool IsReady() { WarmerPlateStatus status = api.GetWarmerPlateStatus(); return status == WarmerPlateStatus.POT_EMPTY; } public override void Start() { isBrewing = true; } }
調用M4UserInterface.CheckButton
系統的控制流程是如何運轉調用CoffeeMakerAPI.GetBrewButtonStatus()函數的呢?選擇線程還是輪詢?這個決策可以在最後一刻作出。這對設計沒有任何影響。最好總是假設訊息都是可以非同步發送的,就好像存在有獨立的線程一樣。
假設我們用輪詢的方式:
public interface Pollable{ void Poll();}
public static void Main(string[] args) { CoffeeMakerAPI api = new M4CoffeeMakerAPI(); M4UserInterface ui = new M4UserInterface(api); M4HotWaterSource hws = new M4HotWaterSource(api); M4ContainmentVessel cv = new M4ContainmentVessel(api); ui.Init(hws, cv); hws.Init(ui, cv); cv.Init(hws, ui); while (true) { ui.Poll(); hws.Poll(); cv.Poll(); } }
public class M4UserInterface : UserInterface,Pollable { private CoffeeMakerAPI api; public M4UserInterface(CoffeeMakerAPI api) { this.api = api; } public void Poll() { BrewButtonStatus status = api.GetBrewButtonStatus(); if (status == BrewButtonStatus.PUSHED) { StartBrewing(); } } }
完成咖啡機練習
20.1.6 這個設計的好處
線條把3個抽象類別圈了起來。圈中的類沒有依賴於任何圈外的類。因此,抽象完全和細節隔離開了。
20.2 物件導向過度設計
這個例子對於教學有很多好處。它短小、易於理解並且展示了如何應用物件導向設計原則去管理依賴和分類關注點。但從另一方面來說,它的短小也意味著這種分離帶來的好處可能抵不過其成本。
如果把Mark IV 咖啡機實現為一個有限狀態機器,我們會發現它有7個狀態和18個遷移。我們可以用18行的SMC(the State Machine Compiler,狀態機器編譯器)代碼來表示狀態機器。輪詢感應器的簡單主迴圈也就十幾行代碼,有限狀態機器要調用動作函數也在幾十行代碼左右。簡而言之,我們可以在一頁代碼之內實現整個程式。
如果不算上測試代碼,咖啡機的物件導向實現有5頁代碼。我們無法對這種懸殊做出合理的解釋。在大型應用中,依賴管理和關注點分離帶來的好處會明顯超出物件導向設計的成本。但是在這個例子中,我們可能得出相反的結論。
摘自:《敏捷式軟體開發 (Agile Software Development):原則、模式與實踐(C#版)》Robert C.Martin Micah Martin 著
轉載請註明出處:
JesseLZJ
出處:http://jesselzj.cnblogs.com
敏捷式軟體開發 (Agile Software Development):原則、模式與實踐——第20章 咖啡的啟示