5.1 設計中的挑戰
- 險惡的問題:險惡的問題:只有透過解決,或部分解決後才能被明確定義的問題。
- 設計遭受到許多限制(語言、Framwork、資源)
- 程式設計本質上是個啟發式的程序:代表你在設計過程中會需要試驗,犯錯;同時你也無法確定這次有用的方式是否在下一個專案也能夠奏效。
5.2 關鍵的設計概念
管理複雜度
所有本質性困難的根源都源自於複雜性。也許可以理解成本質性的問題,目前很難用特定方式來拆解它。 人類更易於理解許多像簡單的訊息。所以所有的軟體設計技術的目標都是把複雜問題分解成簡單的部分。
降低依賴的目的就是要讓你能夠更專心於某層次的問題上。 從問題的領域著手去編寫程式,而不是從底層的實作細節。
管理複雜度的方法
- 在同一時間需要處理的本質複雜度的量減到最少
- 不要讓附屬的複雜度無謂的快速增長
如果以車的例子來說,同個時間,你就不要同時去處理引擎、傳動系統 也不要讓車門的複雜度越來越多(各種的客製化)
理想的設計特徵
- 最小的複雜度:避免做出聰明的設計,應該做出簡單且易於理解的設計。
- 易於維護
- 鬆散耦合:讓程式各個組成部分之間的關聯最小化。
- 精簡性:一本書的膗甭無視在它不能再加入任何內容的時候,而是在不能在刪去任何內容的時候。
- 層次性:層次性意味著盡量保持系統各個分解層的層次性。舉例來說,你在處理網路回覆的層次上,在比較高的層次上不應該知道錯誤是來自於 400 還是 500 等等。
設計的層次
軟體系統
子系統或套件
在這個層次上最重要的是各子系統之間互相通訊的規則(或是依賴規則)
子系統間互動關係的複雜程度(越下面越複雜)
- 一個子系統去呼叫另一個子系統的程式
- 一個子系統包含另一個子系統的類別
- 一個子系統去繼承另一個子系統的類別
常見的子系統
- 業務規則
- 使用者介面
- 資料庫存取
- 對系統的依賴
類別
物件與類別的差別:物件(object)是指 run time 實際存在的實體;而類別是指在程式原始碼中存在的靜態事物。
子程式與子程式內部設計
總結
定義系統 -> 分解組成系統所需的子系統 -> 找出子系統的特徵歸類成類別 -> 找出該類別會有的行為建立子程式 -> 實作該子程式 然後進一步的限制個子系統間的溝通關係 -> 完成系統
5.3 設計構造塊:啟發式設計
找出現實世界中的物件
- 辨識物件及其屬性
- 定義可對物件執行的操作
- 定義每個物件可以對其他物件的操作:包含或繼承
- 確定物件的哪些部份對其他物件可見
- 定義每個物件的介面:實作上其實很繁瑣 QQ
形成一致的抽象
抽象是一種讓你關注某一個概念的時候,可以放心地忽略其中一些細節的能力。 以複雜度的觀點來看,抽象的好處在於它能夠讓你能夠忽略無關的細節。
封裝實作細節
封裝幫助你管理複雜度的方法是,不讓你看到那些複雜度。
當繼承能簡化設計時就繼承
繼承的好處在於它能搭配抽象概念來運作。 舉例來說,手機都能夠打電話。 但是有可能有許多不同種功能的手機,比如說有的手機除了打電話還能傳訊息,有的手機除了打電話還能看影片。
隱藏秘密
資訊隱藏用來降低複雜度(因為他把你不該知道的隱藏起來了) 好的資訊隱藏對於減少「因更動而影響的程式碼數量」是非常重要的
舉例來說
// 方法一
var id: Int = 1
// 方法二
class IdGenerator(){
priavte var id: Int = 1
fun getNewId(): Int{
return id
}
}
全域資料的問題:你無法確定是不是有其他人也在對該資料進行操作。
養成問「我該隱藏什麼?」有助於進行良好的設計。
找出容易改變的區域
好的程式設計所面臨的最重要的挑戰之一就是適應變化。目標應該是要把不穩定的區域給隔離出來,進而限制變化所影響的區域
面對變動的措施
- 找出看起來容易變化的項目
- 分離容易變化的項目:將容易變化的元件劃分成類別
- 隔離容易變化的項目:使用介面
看起來容易發生變化的區域
- 業務規則
- 對硬體的依賴
- 輸入和輸出
- 狀態變數
- 盡量不要使用布林變數作為狀態變數,可以嘗試使用列舉類別
- 使用 access routine 取代對狀態變數的直接檢查?
預料不同程度的變化
優秀的設計者還能夠預測因應變化所需的成本 不要把過多精力浪費在那些不太可能發生,而且又很難做出計畫的變化上
保持鬆散耦合
以下所說的模組,泛指一般的 class or function
好的耦合關係:能讓你的模組能夠靈活地被其他模組使用。
好的例子: sin() 不好的例子:InitVars(var1, var2, var3….) 呼叫端要傳入許多參數,某種程度上就代表呼叫方知道該模組在做什麼
耦合標準
- 規模:以耦合度來說,小就是美。
- 可見性:兩個模組之間連接的顯著程度。
- 好的做法:透過參數列表傳遞資料?
- 壞的做法:修改某種全域變數,做溝通。
- 靈活性:一個模組越容易被其他模組呼叫,那麼他們之間的耦合關係就越鬆散。 (可以理解成需要越少參數的模組,就越靈活?)
耦合的種類
- 簡單資料參數耦合:可被接受
- 簡單物件耦合:可被接受
- 物件參數耦合
class A
class B(private val a: A)
class C{
val a = A()
val b = B(a)
}
語意上的耦合
- module 1 向 module 2 傳遞一個控制標誌,用它來告訴 module 2 該做些什麼。
- module 2 在 module 1 修改某個全域變數後使用該資料。
- 使用 observer pattern ?
- module 1 的介面要求在呼叫 module1.routine() 前要呼叫, module1.init(),但 module 2 在實例化 module 1 後直接呼叫 module1.routine()。因為他知道 module1.routine() 會呼叫 module1.init()
- 解決方法也許是在 module1 介面不要出現 init() ?
- module 1 把 BaseObject 傳給 module2 。由於 module 2 知道 module 1 實際上是給他 DerivedObject ,所以把 BaseObject 轉換成 DerivedObject 。
鬆散耦合的關鍵在於,一個有效率的模組,應該對模組功能的抽象之外,再多提供一層的抽象。 (我的理解:建立一個 class 時你會對他的功能做抽象(也就是建立 function),在這之後,我們可以建立 interface 來提供更高層次的抽象)
查閱常用的設計模式
**應用模式的另一個潛在陷阱「為了模式而模式」。不要僅僅因為想試用某個模式、不考慮該模式是否適合就去應用它。
其他的啟發式方法
- 高內聚力:類別內部的子程式是否支援同一個中心目標(概念完整性)
- 建構分層結構
- 分配職責
- 為測試而設計
- 有意識地選擇綁定時間?
- 畫一張圖
- 保持設計的模組化:模組化的目標是使得每個子程式或類別看起來像是個黑盒子,你知道要進去什麼,也知道出來什麼。但不知道裡面發生了什麼事。
使用啟發式方法的原則
How to Solve it
- 理解問題
- 設計一個計畫。找出現有資料和未知數之間的聯繫。
- 執行計畫
- 回顧。檢視解決的過程。
有效使用啟發式方法的原則是:不要卡在單一的方法。 如果你已經嘗試了一些設計方案,而這些方案無法在當時解決問題的話,更自然的方式是,讓那些問題停留在未解決的狀態,等到你擁有更多資訊之後再去做。
5.4 設計實踐
由上而下和由下而上的設計方法
- 由上而下的設計方法:從某個很高的抽象層次開始,先定義出基底類別或其他不那麼特殊的設計元素。隨著開發過程逐漸增加細節的層次。
- 由下而上的設計方法:始於細節,像一般性延伸。先找出具體物件,根據物件的關係找出基底類別。
由上而下設計的強項:延後建構的細節。 由下而上的設計強項:通常能夠較早找出所需的功能,進而帶來緊湊結構合理的設計。?
以上兩種方法並非互斥你可以受益於兩種方式的互相協作
建立試驗性原型(prototyping)
建立原型:寫出用於回答特定設計問題的、數量最少且能夠隨時扔掉的程式碼。
此方法可能失敗的原因:
- 開發者沒有依照「用最少程式碼回答提問」
- 設計問題不夠明確
- 開發人員沒有把原型程式碼當作可以被拋棄的程式碼。
要做多少設計才夠
最大的設計問題通常不是來自於那些我認為很困難,並且做出了不好的設計的區域;而是來自於那些我認為很簡單,但沒有做出任何設計的區域。