04/02 23:21~ 04/03 01:28
最近 Teddy 讀了『別為我解釋印度』這本書,其中有一章關於印度人如何
推卸 釐清責任的敘述,十分有趣,以下節錄幾段:
『Madam, 請聽我說,這是你的問題,不是我的問題』... 我(作者)第一項領教到的便是印度人事事分的清清楚楚的習性,像這樣的話語不斷地出現在我們耳邊...
旅館經理:『Madam, 請聽我說,飛機誤點太久或停飛不是我的問題,那並不是我造成的,如果飛機誤點,我能做的只是請司機再帶你們回來而已,其實那是你們自己的問題!』
作者:『什麼叫飛機停飛是我們的問題?我請你幫我們確認航空公司當日起飛的時間是否有更改,你也告訴我航空公司說一切都和原訂時刻相同,現在你又告訴我如果飛機停飛是我的問題?』
旅館經理:『Madam, 請聽我說,全世界沒有人會知道飛機會不會出問題,會不會誤點或停飛,你要我怎麼向你保證航空公司說當天的飛機沒有問題,到時候飛機真的就一點問題都沒有?』
作者:『所以我應該要花錢坐特別貴的計程車到機場,發現飛機停飛,然後就乖乖等在機場,等到三天後飛機再來?這就是我的問題?』
旅館經理:『Madam, 是的!這就是你會遇上的問題,但是你這個問題我很樂意幫你解決,那就是請計程車司機等在機場外,萬一你們去到機場發現班機時間突然改了,你們就可以再坐計程車回來,您同意嗎?』
作者:『我開始有點佩服這個經理了...』
***
當程式執行時發生了例外,首要之務就是要找到例外發生的
根本原因(root cause),其次,就是要釐清
『誰該負責』來處理這個例外狀況。今天要談的是誰該負責的問題。所謂『冤有頭,債有主』,如果找到不該負責的人來處理例外,那麼程式很可能會變得不易理解也不容易維護,弄不好也可能會導致更多的錯誤發生。
舉個例子,假設你要設計一個讓使用者透過網頁輸入個人資料然後將資料儲存到資料庫的功能,其中有一個欄位是輸入『年齡』。我們都知道,一般正常的人類應該是不可能超過 200 歲(『彭祖』和偷吃了長生不老藥的『嫦娥』除外),所以你在資料庫中將年齡這個欄位的長度設為 unsigned short (0-255)。除非科學家發明了長生不老藥,否則 255 應該很夠用了。
你為這個功能設計了四個元件:
- UI:顯示網頁的程式碼。
- UserBean:用來將使用者輸入的資料由 Web UI 傳送到資料庫。
- Servlet:呼叫 UserDAO 將由 UI 端收到的 UserBean 存到資料庫中 。
- UserDAO (Data Access Object):負責透過 JDBC 或是 OR-Mapping 工具將 UserBean 存到資料庫中 。如果儲存資料發生錯誤,將丟出 exception。
程式開發完畢,很幸運的你找來『彭祖』幫你作測試,『彭祖』在『年齡』這個欄位輸入 888,按下確定送出之後,畫面上看到由資料庫所發出的 exception,內容類似『integer out of range ...』 這一纇的訊息。好了,這個例外,是誰的問題?
- UI:Madam, 請聽我說,這不是我的問題。誰叫你資料庫欄位設計的太小,改成 unsigned int (0-65535) 不就好了。
- UserBean:Madam, 請聽我說,很明顯的這不是我的問題,因為我的 setAge() 和 getAge() 接受和傳回的都是 Int,範圍大於 888。
- Servlet:Madam, 請聽我說,這絕對不是我的問,我只是負責把收到的 UserBean 傳給 UserDAO。
- UserDAO :Madam, 請聽我說,這肯定不是我的問題。依照規格,年齡欄位最大值就是 255。有人要輸入超過這個數值的資料,當然一定會發生例外,如果不丟出例外才是我的問題。
和這個例子類似的狀況屢見不鮮,只要有寫過資料庫程式的鄉民們應該都會遇到。一般來說 UI 應該依據資料庫可接受值的範圍來做檢查,以避免將不正確或無法接受的資料存到資料庫。問題是,第一個丟出例外的人是 UserDAO,所以,如果你是設計 UserDAO 的人,你如何決定要自行處理這個例外(例如,用一個預設的數值來代替),或是往上丟交給別人處理?個時候就是要學學印度人釐清責任的功力,休息一下,接著看下去。
***
設計軟體元件(methods, functions, classes, or components)的目的就是要提供某種服務,因此當程式在執行的時候,軟體元件之間的互動關係,便可簡單區分為以下兩者:
- Client:呼叫其他元件以便獲得某種服務。
- Supplier:提供服務的元件。
有了 client 和 supplier 的概念之後,要幫軟體系統釐清責任的方法就變得很簡單了,只要幫元件介面撰寫『合約』(contract) 就可以了。合約有兩種 (其實有三種,為了避免 Teddy 打字打到手痛,這邊先看兩種就好),分別是:
- Precondition:在執行某個軟體元件之前,必須要滿足的條件。當 client 要呼叫 supplier 之前,client 必須要保證定義在該 supplier 的 preconditions 有被滿足,才可以呼叫 supplier (也就是說 client 必須提供執行 supplier 所需的環境)。
- Postcondition:在執行某個軟體元件之後,必須要滿足的條件。當 client 要呼叫 supplier 之後,supplier 必須要保證定義在該 supplier 的 postconditions 有被滿足 (也就是說 supplier 提供了他所宣稱要提供的服務)。
當程式執行時,如果合約被違反了,系統將會丟出 exceptions。Precondition violation exceptions 表示 client 有 bugs,而 postcondition violation exceptions 則表示 supplier 有 bugs。
故事講到這邊,如果鄉民們還看的懂那麼 Teddy 先給你拍拍手...再回到前面儲存使用者資料的例子,有了合約的概念,UserDAO 的合約大概長成這樣子:
public void saveUser (UserBean aBean)
Precondition:
require aBean.getAge() >= 0 && aBean.getAge() <= 255
Postcondition:
ensure DBUtil.getUser(aBean.getName()).equals(aBean) // 表示資料成功存到資料庫
有了這個合約,如果使用者在 UI 的年齡欄位輸入 888 而 UI 又沒有做檢查的情況下,當 Servlet 呼叫 UserDAO.saveUser() 時便會出現 precondition violation exception。有了合約之後,這是誰的問題就很清楚了。由於 UserDAO 已經明白表示『如果要呼叫我, age 的值一定要介於 0-255』,所以根本也不用考慮是否需要修改資料庫欄位來接受大於 255 的數值(因為違反 preconditions 是 client 的問題,不是 supplier 的問題)。
以此類推,如果 Servlet 和 UserBean 都有類似的合約,那麼當使用者在畫面上的年齡欄位輸入 888,就可以立刻知道這是 UI 的問題,不是別人的問題了。那麼,UI 的合約要怎麼寫?
Precondition:
require true
Postcondition:
ensure A valid UserBean is created and saved in the session
UI 畫面是要讓使用者輸入資料的,所以只要使用者執行這個功能就可以用,因此 precondition 永遠成立(當然你也可以寫成 require a user is logged on 之類的)。既然 UI 的目的是要收集使用者資料,所以在畫面結束之後(假設使用者按下確定送出),要保證能夠產生一個合法的 UserBean 物件並將它存在 session。因此,為了滿足這個 postcondition,很顯然的 UI 的實做就必須要檢查使用者所打的欄位是否正確。也就是說,使用者輸入資料的 validation 要做在 UI 端。
友藏內心獨白:這就是 Design by Contract 的觀念啦!