程式設計一定有耦合, 專案運作有簡單有複雜, 開發前應詳閱SOLID設計原則 | 軟工基本功

工作一開始只懂得把主管交辦任務實作出來
前期都是前輩或主管著手規劃與設計
但人總是會進步的


到後期也要著手設計系統與實作
甚至需要設計系統後給後輩同事進行開發
這時候就會開始思考我的設計方式是否有問題?
我是不是能勝任系統設計?
因此就會踏上尋找軟體設計的原則或方法
一開始找到是23種設計模式, 5種設計原則

 

沒遇到 SOLID設計原則前可能會遇到的狀況

  • 什麼東西都寫在同一個類別中, 多人合作衝突讓你排除到飽
  • 創造更多就業機會! 人體臭蟲製造機, 改A壞B,C,D
  • 無限多層的父類別, A繼承B, B又繼承C,D, C又繼承E, F, G
  • 無法擴充新的套件, 因為現在的系統只能用某一個版本的套件, 升上去會炸掉
  • 突然來一個超緊急案子, 設計什麼,摻在一起做成一個類別啊!
  • 這功能好像跟上次做的一樣欸?
  • 這之前做過開發起來應該很快吧?
  • 不是程式碼複製貼上, 稍微改一改就好了嗎?

常常寫程式都會懷疑人生…

 

遇到 SOLID設計原則後

  • 會思考模組化特性為「獨立性」與「重複使用性」
  • 未來可依據個別需求擴充, 也保證模組與模組之間互相兼容
  • SOLID在講的精神為面對原始碼改變的策略

 

SOLID 5+1大設計原則

單一職責原則, Single Responsibility Principle (SRP)

  • 說明
    • 不要貪圖方便就全部塞在一起, 每一個物件只接收單一任務
    • 設計階段要注意不要野心過大, 讓單一物件職責變多
    • 依照模組定義或行為來拆開類別的職責
    • 降低單一物件被改變所影響的機會 (改A壞B)
    • 單一物件中少寫 If else / switch
    • 關注點分離, 達到程式碼高內聚
  • 好處
    • 容易撰寫單元測試
    • 提高程式碼可讀性與可維護姓
    • 發生問題可以快速找到錯誤的地方
  • 潛在問題
    • 一個未知需求, 無法量化需求的單一職責
    • 劃分太細會一大堆介面, 可能會踩到 LSP 的雷
    • 火燒屁股型專案開發公司, 專案時程會拉長
  • 錯誤範例
    • 第一次寫購物車就什麼功能全塞到同一個Class
      • 購物車增加品項
      • 購物車移除品項
      • 購物車查詢品項
      • 訂單處理
      • 付款處理
      • 物流處理
      • 寄送確認信處理
  • 正確範例
    • 購物車的處理應該獨立一個Class
      • 購物車增加品項
      • 購物車移除品項
      • 購物車查詢品項
    • 其他的處理也個別獨立Class
      • 訂單處理
      • 付款處理
      • 物流處理
      • 寄送確認信處理

開放封閉原則, Open Close Principle (OCP)

  • 說明
    • 設計階段只考慮抽象層級的介面互動, 把變化委託給其他類別處理
    • 擴展保留開放, 修改保持封閉
    • 程式面可以使用抽象來達到不變動模組又可以改變行為
    • 實作的部分可以搭配相依注入方式來擴充新需求
  • 好處
    • 容易管理維護一個行為, 不同實作
    • 應用在大型或複雜專案會非常有感覺
  •  潛在問題
    • 一開始可能無法預想需求會擴充, 但後期可透過重構調整
    • 專案初期遵照OCP成本會非常高, 也會增加複雜度
  • 錯誤範例
    • 購物車加入商品時, 需依照不同促銷活動處理, 這些促銷活動很難確定類型
      • 雙11促銷活動
      • 情人節促銷活動
  • 正確範例
    • 購物車加入商品時, 兩個活動可使用抽象或介面定義行為, 最後要套用哪一種活動, 再經由購物車加入商品時決定

里氏替換原則, Liskov Substitution Principle (LSP)

  • 說明
    • 避免父類別被子類別繼承所造成的行為改變 (原本回傳布林, 突然變throw error)
    • 子類別必須完全實作父類別方法, 方法與回傳值與丟出的異常都必須一致
    • 覆寫或實作父類別時, 參數與回傳值要與父類別定義相同
  • 好處
    • 增加程式碼的強健度
    • 版本升級時候相容性會更好
  • 錯誤範例
    • 購物車加入商品時, 會查看商品的狀態決定是否
      • 導回購物車列表頁面
      • 或是直接導回500錯誤頁
  • 正確範例
    • 購物車加入商品時, 會查看商品的狀態決定是否, 應該都必須統一導回某一個頁面, 不應該模稜兩可

最小知識原則, Least Knowledge Principle (LKP)

  • 說明
    • 避免曝露過多資訊, 造成流程調整而改變
    • 盡可能減少要知道的類別, 降低類別對陌生類別的耦合度
    • 需要操作內部類別的流程就封裝成 Public Method
    • 外在類別對內部類別細節知道越少, 對內部類別耦合度就會越低
  • 好處
    • 設計API時候可讓使用的工程師更容易理解使用
    • 可以很明確理解單一API做一件動作 (不會過於複雜難理解維護)
  • 錯誤範例
    • 訂單結帳時候, 把所有結帳流程寫在 Controller
      • ATM結帳
      • Apple Pay結帳
  • 正確範例
    • 訂單結帳時候, Controller只要知道我呼叫一個結帳方法, 然後再經由結帳方法調用結帳的方式 (ATM or Apple Pay)

界面隔離原則, Interface Segregation Principle (LSP)

  • 說明
    • 降低用戶端因為不相關介面而被改變
    • 程式碼不應該依賴用不到的介面
    • 單一介面只提供一個商業邏輯為主
    • Interface 應該要認知為可以做什麼, 而不是是一個什麼
    • 發現一個類別實作都是空的, 表示 Interface 可以再拆細一點
  • 好處
    • 讓系統解耦合, 後續容易重構
  • 錯誤範例
    • 維護舊的電商平台, 遇到訂單完成會發送確認信, 發送信的參數竟然需丟訂單介面, 這時候如果不是訂單的方法需要發信, 就要實作訂單介面
  • 正確範例
    • 應重構發信的元件, 將原發信傳入值改成傳入發信介面, 而不是訂單介面

依賴反轉原則, Dependency Inversion Principle (DIP)

  • 說明
    • 避免高階程式因為低階程式改變而被迫改變
    • 高階模組不應該依賴低階模組, 兩者應該都要依賴抽象
    • 抽象不要依賴細節, 細節要依賴抽象
    • 不要把程式碼寫死在實作上
    • 設計階段就可抽象層次的互動, 進而避免相依問題
    • 抽象層次不要太高, 以免無法對焦真正的關注點
    • 互動的部分交給抽象類別或介面
    • 會改變的實作, 就放到子類別裡面實作就好
  • 好處
    • 避免應用模組因底層模組被改變, 強迫全部被改變
  • 錯誤範例
    • 舊程式付款方式只有一種ATM, 未來要新增其他付款方式, 就只能用if else / switch 去切換
  • 正確範例
    • 付款這個動作不應該要實作, 付款這個動作應該交給介面定義就好
    • 實際做再交給其他繼承付款方式的子類別實作就好

結論

開發一定有耦合, 專案運作有簡單有複雜, 開發前應詳閱公開設計原則

 

參考

  • http://slides.com/jaceju/design-patterns-by-examples
  • http://cloverhsc.blogspot.com/2019/03/solid.html

留言

Top