物件導向中常見的幾個設計原則 - Some Principle of Object Oriented Design

隨著參與大型專案的開發次數越來越多,對維護與穩定的要求越來越高,我不斷地感受到,改變一下程式的設計方式或許不影響程式功能,但是在未來修改或擴增功能時卻會變得更加容易。甚至好的設計之下,可以將部分的程式碼作點修改,就能適用於不同的專案,大大地節約開發所需要耗費的時間跟心力。

針對於好的設計,最近藉由一本書在學習設計模式,書中有一小節簡短帶過了一些關於進行物件導向開發時,可以讓開發或者維護更容易的一些設計原則。

雖然只是短短一小節,但是就讓我體會到站在巨人肩膀上的感覺。過去為了讓自己的程式有著上述容易維護與可重複使用的優點,往往會花上許多時間去反覆思考,但原來早就有前人歸納出幾個原則可以作為設計的大方向了,我只是再次考慮了相同的問題而已。

這些原則不能說是絕對的,但是如果在這些原則所指名的方向上思考,可以確實的讓人少走上許多冤枉路。上網特別研究了在書中提到的幾項設計原則之後,留下了此篇紀錄。(六項原則:SRP、OCP、LSP、ISP、DIP、LKP)

SOLID (SRP, OCP, LSP, ISP, DIP)

首先是由 Robert Cecil Martin 在 Agile software development (2002) 一書中提到的 SOLID 五個設計原則:

  • Single responsibility principle (SRP)
  • Open/closed principle (OCP)
  • Liskov substitution principle (LSP)
  • Interface segregation principle (ISP)
  • Dependency inversion principle (DIP)

單一責任原則 SRP

一個類別 (class) 只負責一項事物,不在一個類別之中實作過多的功能。

開放封閉原則 OCP

一個類別應該兼具開放性與封閉性,開放性指一個類別應該可以因應需求而被擴展,封閉性則指一個完成的類別不應該為了需求而被修飾。

看起相互矛盾的兩個要求,如何被同時實現呢?透過將類別拆分成抽象類別及實作類別,而實作類別會繼承於抽象類別,便可以達成要求。

當類別的功能被使用時,皆透過抽象類別來呼叫,抽象類別可以被固定下來達成封閉性;當需求改變時,只要重新實作一個作為子類別的實作類別,便可以在保留介面(抽象類別的函式不變)的情況下修改功能,擁有開放性。

里氏替代原則 LSP

由 Barbara Liskov 在 1988 年提出的:“Subtypes must be substitutable for their base types.”

翻譯之後的意思是指,子類別必須能夠替代父類別。詳細一點來說,便是指一個子類別的實作,應該包括父類別的所有介面(函式),使子類別被使用時,可以完全取代父類別的定位,不會有短少功能的情況。用C#的角度來解釋,就是父類別的所有 public function  應該都要是 virtual 或是 abstract 的。

介面分離原則 ISP

與其設計一個結合所有功能的介面。不如設計好幾個因應需求的介面,讓使用者可以在擁有最少介面的情況下操作功能。簡化介面與切分功能便是這個原則的表現。

依賴性反轉原則 DIP

這個原則包含兩點:

  1. 高階模組不應該依賴低階模組,兩者應依賴於抽象類別。
  2. 抽象類別不應該依賴實作類別,而實作類別則應該依賴抽象類別。

所謂的不依賴,簡單來說就是,低階模組被替換或修改後,高階模組不應該被迫修改;反過來說,如果抽象類別被修改了,則實作類別被迫修改則是合理的。而如何區分高階跟低階?通常低階模組會是高階模組的一部份。

最少知識原則 Least knowledge principle (LKP)

在1987年被提出,可用三點來概括:

  1. 每個單位與對其他單位有最低限度的了解,關於跟自己有關的那部分。
  2. 每個單位只能跟朋友溝通,而非陌生人。
  3. 上述的朋友,只包括直接的朋友。

這三句話講得文謅謅的,相當哲學。這個原則書上的解釋也不多,我的理解是:在實現類別的功能時,盡可能不要去依賴其他類別的成員變數,需要甚麼資訊都盡量自行取得,降低每個類別之間的耦合度,免得維護時牽一髮則動全身。

Inheritance (IS-A) vs. Composition (HAS-A)

**多用組合少用繼承。**這點被沒稱之為原則,但是許多關於物件導向程式設計的討論都會提到這點,可以說是大部分的人都認同的經驗談。 物件導向的繼承很方便,但是良好的設計不應該過度依賴繼承,繼承的層數應該不要超過三層,除了效能考量,也能避免在追蹤 bug 時,需要大量的時間來抽絲剝繭。甚至其實有些語言根本不支援多重繼承。透過用組合代替繼承,可以把一些沒必要沿用的功能保留下來,減少新類別所擁有的溝通接口,維護上也不會被迫整頓一些用不到的函式。   相關參考