12.07.2014

Java的十個物件導向設計原則

對於開發者而言,想必大家都知道design pattern的重要性;但這篇不是要講design pattern,我們要來看看物件導向程式設計的基本原則,其中包含了SOLID(Single responsibility, Open-closed, Liskov substitution, Interface segregation 和 Dependency inversion),這些基本原則可以幫助你寫出更簡潔、易維護、低耦合的系統。

不要自我重覆(DRY:Don't Repeat Yourself)

不要寫出重覆的程式(功能)!若有一部份的程式出現在不同地方,你可以考慮使用抽象化(Abstraction: 將相同的邏輯往parent搬)或委派(Delegation:將相同的任務委派給另一個物件,或另一個method完成),若有hard-code value重覆出現,則使用public final取代之,藉此可以提升維護性
這個原則很重要,但要小心不能濫用,這裡的程式並不是指程式碼,而是指功能。假設我們使用同樣的程式來驗證資料A以及資料B,雖然現在A及B有著相同格式,若我們使用委派,將相同的驗證抽成一個共用method,代表我們認為A和B的格式永遠不會變動,一旦未來其中一個格式有變動,改了驗證程式就會造成另一個資料驗證問題(若沒有Unit test特別容易發生)。因此要特別注意,這個原則適用在相同的功能而不是相似的程式


將變動封裝起來(Encapsulate Changes)

軟體開發中唯一不變的就是”變動”,變動可能來自於需求的改變、使用不同的工具等,要減少變動影響範圍,我們要將可能會變動的程式封裝起來。例如要發動車子,內部有很多零件必需協同和作才能完成,但用戶不需要知道這些細節,他們只需要將鑰匙轉動即可。由於封裝將細節隱藏起來,只提供主要介面供使用者驅動,若內部實作細節有任何異動,用戶端程式可以不用做任何異動。封裝簡單說就是將變數及method儘可能private化,至於要怎麼判斷程式碼是否有做到封裝,依我的經驗,若有段程式從物件A get不同參數值或呼叫A的不同method,很可能這段程式是屬於A的責任,你已經違反了封裝的概念。不要小看封裝,若你的實作散落各地,實作內容變動愈大,你所要修改的類別可能愈多

開放關閉設計原則(Open Close Design Principle)

有點難理解的原則,又開放又關閉的!其實指的是不同東西,開放指的是類別及方法要能夠擴充(加新功能或不同實作),關閉指的是關閉修改(refactoring例外)。理想上當你要加入新功能,應該引入新的類別、方法或變數,而不是去修改既有已經完成測試的程式,這可以避免修改造成的regression bug。
但要如何使程式能夠擴充呢?基本上我們可以運用多型(polymorphism),藉由相同介面(interface或super class),讓我們可以在擴充行為時不影響client端以及既有的程式

單一責任原則(Single Responsibility Principle)

責任指的是變動的理由,一個類別應該只有一個變動的理由例如一個類別負責編輯報表以及列印報表,這個類別就有二個變動的理由;一個是資料的異動,另一個是報表的格式異動。將二個不同責任綁在一起,除了會造成閱讀困難外,也會使其不好維護,畢竟我們不能保證修改其中一個功能時,對另一個功能不會造成任何影響。


相依性注入或反轉原則(Dependency Injection or Inversion Principle)

相信有使用Spring Framework的都對這個原則不漠生,一般我們要使用一個物件可能會直接new 一個instance,但DI告訴我們不要這樣,你應該動態注入相依的物件,這種方式可以帶來下面二種好處:
  1. 讓我們在撰寫測試時可以利用mock object,讓我們專注在該測的功能上
  2. 可藉由修改設定檔注入不同實作(Polymorphism),而不必修改程式。

傾向使用Composition多於繼承(Favor Composition over Inheritance)

使用繼承我們可以將共同的程式碼移至super class,使用composition我們則是將共同的程式碼移至composed class,在不考慮使用繼承是不是好的設計狀況下,二種差別在於彈性:使用繼承我們無法動態變動實作,使用composition,我們可以藉由polymorphism,讓不同實作有相同介面,藉由執行期的相依性注入抽換不同實作,任何時候我們都可以切換不同的實作且不用改程式。

介面分離原則(ISP:Interface Segregation Principle)

這個原則指的是client不應實作一個他不提供的功能介面會違反這個原則,大多是因為一個介面負責了超過一個功能(違反單一責任原則),而client只需要其中部份功能。在介面的設計上必須特別小心,因為一旦發佈了後,日後所做的任何修改都會使現有的實作無法運行。

Liskov 替換原則(Liskov Substitution Principle)

這個原則指的是子類別的實作必需可以替代父類別!也就是說父類別的實作必需在子類別也能運作LSP和單一責任原則以及介面分離原則有很強的關聯,若一個類別負責許多許責任,子類別一旦無法支援所有行為,就會違反LSP。要遵守LSP,則子類別必須是加強父類別的功能,不能減少。舉個教科書常見的錯誤例子來說:父類別Rectangle定義了長及寬變數,以及取得面積的方法,子類別Square繼承了父類別的長及寬變數以及行為,當我們以polymorphism的角度使用Square類別(Rectangle r = new Square())時,使用者認為他必需設定長及寬來計算面積,但當他設定長=10,寬=5並計算面積時,發現回傳是25(因為正方形四邊都一樣)而不是50!因為polymorphism開發時操作的是super class或interface,因此違反了這個原則可能帶來用戶錯誤的預期

針對介面而非實作(Programming for Interface not implementation)

這個原則其實就是要我們善用polymorphism,定義變數或方法變數或回傳變數時使用介面而非實作可以帶來彈性,當要替換不同實作時,這可以讓你減少異動

委派原則(Delegation Principle)

不要試著在一個類別中做所有的事,把責任委派給該負責的類別。例如hash code和equals method,要比較二個物件是否相同,我們並不會在client端寫程式去比,而是交由二個物件本身自己去比較,這樣的好處是可以減少重覆的程式碼,讓系統更好維護。

你可能對下面主題有興趣:
  1. [OO概念]封裝,繼承,多型
  2. [Java 概念]Interface and abstract class

No comments: