引子:拍攝 UFO 失敗的手機#
某天散步時,小菜與大鳥目擊一個疑似 UFO 的飛行物。小菜興奮地拿出新手機錄影,但畫面只剩黑色背景中的一個小白點,根本無法當證據。
大多數時候,一件產品簡單一些、職責單一一些,或許是更好的選擇。
智慧手機什麼都能做,但拍攝品質遠不如專業攝影機(DV)。這正是設計模式中單一職責原則的道理。
單一職責原則(SRP)#
單一職責原則(Single Responsibility Principle, SRP):就一個類別而言,應該僅有一個引起它變化的原因。[ASD]
常見的反例:
- 寫一個視窗應用程式,自然地建立
Form1類別 - 把商業運算、資料庫存取(SQL 語句)、UI 邏輯通通寫在這一個類別裡
- 任何需求變動都得改這個類別 → 維護困難、無法複用、缺乏彈性
範例:方塊遊戲(俄羅斯方塊)#
如果用 WinForm 開發俄羅斯方塊,初學者通常把所有程式碼都寫在 Form1.cs:
Form_KeyDown處理鍵盤timer_Tick處理動畫- 繪製方塊用 GDI+ 寫在按鈕事件中
- 碰撞、堆積、消層判斷也擠進去
問題:手機版(Pocket PC、Windows CE)需要重寫,因為 PC 上的 WinForm 程式無法直接使用。
找出可分離的職責#
始終沒變的,是遊戲邏輯:下落、旋轉、碰撞判斷、移動、堆積、消層。這些與介面如何呈現無關,不應與介面寫在一起。
具體分離方式:
- 用一個二維整數陣列
int[,] arraySquare = new int[10, 20]表示遊戲區域 - 方塊位置就是陣列的下標變化
- 碰撞判斷:
arraySquare[x-1, y]是否為 1 - 堆積判斷:
arraySquare[x, y+1]是否為 1 - 消層:某一列全為 1 時清零,並將上方資料下移
於是程式至少分為兩個類別:
- 遊戲邏輯類:操作陣列的值變化
- WinForm 視窗類:根據陣列繪製、根據鍵盤命令呼叫遊戲邏輯
flowchart LR
subgraph 表示層[視窗類 / 介面層]
F[Form / KeyDown / Timer]
D[繪製方塊]
end
subgraph 邏輯層[遊戲邏輯類]
L[陣列狀態 + 移動 / 旋轉 / 碰撞 / 堆積 / 消層]
end
F -->|呼叫| L
L -->|回傳狀態| D當介面或平台改變時,只需改視窗類,遊戲邏輯不受影響。
如何判斷該分離?#
軟體設計真正要做的許多內容,就是發現職責並把那些職責相互分離。[ASD]
判斷準則:如果你能夠想到多於一個的動機去改變一個類,那麼這個類就具有多於一個的職責,就應該考慮職責分離。[ASD]
耦合的代價#
如果一個類承擔過多職責:
- 等於把這些職責耦合在一起
- 一個職責的變化可能會削弱或抑制這個類完成其他職責的能力
- 這種耦合導致脆弱的設計,變化發生時,設計會遭受意想不到的破壞
整合 vs. 單一職責#
整合與單一職責並不對立,要看情境:
- 整合:Google 最初將一切需求整合到一個搜尋框,用乾淨頁面吸引用戶
- 單一職責:分類資訊、垂直搜尋的興起,又體現了單一職責的思想
- 現在智慧手機整合多功能,是因為 DV、DC、MP3 體積太大;如果每樣產品都縮小到口袋大小、品質不變,整合反而沒必要
對軟體設計而言,重點是在類別的職責分離上多思考,做到單一職責,這樣的程式碼才能真正易維護、易擴展、易複用、靈活多樣。