5.1 設計中的挑戰

  1. 險惡的問題:險惡的問題:只有透過解決,或部分解決後才能被明確定義的問題。
  2. 設計遭受到許多限制(語言、Framwork、資源)
  3. 程式設計本質上是個啟發式的程序:代表你在設計過程中會需要試驗,犯錯;同時你也無法確定這次有用的方式是否在下一個專案也能夠奏效。

5.2 關鍵的設計概念

管理複雜度

所有本質性困難的根源都源自於複雜性。也許可以理解成本質性的問題,目前很難用特定方式來拆解它。 人類更易於理解許多像簡單的訊息。所以所有的軟體設計技術的目標都是把複雜問題分解成簡單的部分。

降低依賴的目的就是要讓你能夠更專心於某層次的問題上。 從問題的領域著手去編寫程式,而不是從底層的實作細節。

管理複雜度的方法

  1. 在同一時間需要處理的本質複雜度的量減到最少
  2. 不要讓附屬的複雜度無謂的快速增長

如果以車的例子來說,同個時間,你就不要同時去處理引擎、傳動系統 也不要讓車門的複雜度越來越多(各種的客製化)

理想的設計特徵

  1. 最小的複雜度:避免做出聰明的設計,應該做出簡單且易於理解的設計。
  2. 易於維護
  3. 鬆散耦合:讓程式各個組成部分之間的關聯最小化。
  4. 精簡性:一本書的膗甭無視在它不能再加入任何內容的時候,而是在不能在刪去任何內容的時候。
  5. 層次性:層次性意味著盡量保持系統各個分解層的層次性。舉例來說,你在處理網路回覆的層次上,在比較高的層次上不應該知道錯誤是來自於 400 還是 500 等等。

設計的層次

軟體系統

子系統或套件

在這個層次上最重要的是各子系統之間互相通訊的規則(或是依賴規則)

子系統間互動關係的複雜程度(越下面越複雜)

  1. 一個子系統去呼叫另一個子系統的程式
  2. 一個子系統包含另一個子系統的類別
  3. 一個子系統去繼承另一個子系統的類別

常見的子系統

  1. 業務規則
  2. 使用者介面
  3. 資料庫存取
  4. 對系統的依賴

類別

物件與類別的差別:物件(object)是指 run time 實際存在的實體;而類別是指在程式原始碼中存在的靜態事物。

子程式與子程式內部設計

總結

定義系統 -> 分解組成系統所需的子系統 -> 找出子系統的特徵歸類成類別 -> 找出該類別會有的行為建立子程式 -> 實作該子程式 然後進一步的限制個子系統間的溝通關係 -> 完成系統

5.3 設計構造塊:啟發式設計

找出現實世界中的物件

  1. 辨識物件及其屬性
  2. 定義可對物件執行的操作
  3. 定義每個物件可以對其他物件的操作:包含或繼承
  4. 確定物件的哪些部份對其他物件可見
  5. 定義每個物件的介面:實作上其實很繁瑣 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)

建立原型:寫出用於回答特定設計問題的、數量最少且能夠隨時扔掉的程式碼。

此方法可能失敗的原因:

  • 開發者沒有依照「用最少程式碼回答提問」
  • 設計問題不夠明確
  • 開發人員沒有把原型程式碼當作可以被拋棄的程式碼。

要做多少設計才夠

最大的設計問題通常不是來自於那些我認為很困難,並且做出了不好的設計的區域;而是來自於那些我認為很簡單,但沒有做出任何設計的區域。