一. Dependency Inversion Principle (DIP) - 依賴倒置原則 java
依賴:在程序設計中,若是一個模塊a使用或調用了另外一個模塊b,咱們稱模塊a依賴模塊b。 設計模式
高層模塊與低層模塊:每每在一個應用程序中,咱們有一些低層次的類,這些類實現了一些基本的或初級的操做,咱們稱之爲低層模塊;另外有一些高層次的類,這些類封裝了某些複雜的邏輯,而且依賴於低層次的類,這些類咱們稱之爲高層模塊。 this
依賴倒置原則的2個重要方針: spa
A. 高層模塊不該該依賴於低層模塊,兩者都應該依賴於抽象
B. 抽象不該該依賴於細節,細節應該依賴於抽象 設計
爲何叫作依賴倒置(Dependency Inversion)呢? code
面向對象程序設計相對於面向過程(結構化)程序設計而言,依賴關係被倒置了。由於傳統的結構化程序設計中,高層模塊老是依賴於低層模塊。 orm
Robert C. Martin在原文中給出了「Bad Design」的定義: 對象
1. 系統很難改變,由於每一個改變都會影響其餘不少部分。
2. 當你對某地方作一修改,系統的看似無關的其餘部分都不工做了。
3. 系統很難被另一個應用重用,由於你很難將要重用的部分從系統中分離開來。
致使「Bad Design」的很大緣由是「高層模塊」過度依賴「低層模塊」。一個良好的設計應該是系統的每一部分都是可替換的。若是「高層模塊」過度依賴「低層模塊」,一方面一旦「低層模塊」須要替換或者修改,「高層模塊」將受到影響;另外一方面,高層模塊很難能夠重用。
好比,一個Copy模塊,須要把來自Keyboard的輸入複製到Print,即便對Keyboard和Print的封裝已經作得很是好,但若是Copy模塊裏直接使用Keyboard與Print,Copy任很難被其餘應用環境(好比須要輸出到磁盤時)重用。 繼承
問題的解決: 接口
爲了解決上述問題,Robert C. Martin提出了OO設計的Dependency Inversion Principle (DIP) 原則。
DIP給出了一個解決方案:在高層模塊與低層模塊之間,引入一個抽象接口層。
High Level Classes(高層模塊) --> Abstraction Layer(抽象接口層) --> Low Level Classes(低層模塊)
抽象接口是對低層模塊的抽象,低層模塊繼承或實現該抽象接口。
這樣,高層模塊不直接依賴低層模塊,高層模塊與低層模塊都依賴抽象接口層。
固然,抽象也不依賴低層模塊的實現細節,低層模塊依賴(繼承或實現)抽象定義。
Robert C. Martin給出的DIP方案的類的結構圖:
PolicyLayer-->MechanismInterface(abstract)--MechanismLayer-->UtilityInterface(abstract)--UtilityLayer
類與類之間都經過Abstract Layer來組合關係。
二. Liskov Substitution Principle (LSP) - 里氏替換原則
全部引用基類的地方必須能透明地使用其子類的對象。也就是說,只有知足如下2個條件的OO設計纔可被認爲是知足了LSP原則:
if (obj typeof Class1) { do something } else if (obj typeof Class2) { do something else }B 子類應當能夠替換父類並出如今父類可以出現的任何地方,或者說若是咱們把代碼中使用基類的地方用它的子類所代替,代碼還能正常工做。
class Rectangle { double width; double height; public double getHeight() { return height; } public void setHeight(double height) { this.height = height; } public double getWidth() { return width; } public void setWidth(double width) { this.width = width; } }
class Square extends Rectangle { public void setHeight(double height) { super.setHeight(height); super.setWidth(height); } public void setWidth(double width) { super.setHeight(width); super.setWidth(width); } }這裏Rectangle是基類,Square從Rectangle繼承。這種繼承關係有什麼問題嗎?
void g(Rectangle r) { r.setWidth(5); r.setHeight(4); if (r.getWidth() * r.getHeight() != 20) { throw new RuntimeException(); } }則對應於擴展類Square,在調用既有業務邏輯時:
Rectangle square = new Square(); g(square);
時會拋出一個RuntimeException異常。這顯然違反了LSP原則。
動做正確性保證:
由於LSP對子類的約束,因此爲已存在的類作擴展構造一個新的子類時,根據LSP的定義,不會給已有的系統引入新的錯誤。
Design by Contract
三. Interface Segregation Principle (ISP) - 接口分隔原則
不能強迫用戶去依賴那些他們不使用的接口。換句話說,使用多個專門的接口比使用單一的總接口總要好。它包含了2層意思:
1)接口的設計原則:接口的設計應該遵循最小接口原則,不要把用戶不使用的方法塞進同一個接口裏。
若是一個接口的方法沒有被使用到,則說明該接口過胖,應該將其分割成幾個功能專注的接口。
2)接口的依賴(繼承)原則:若是一個接口a依賴(繼承)另外一個接口b,則接口a至關於繼承了接口b的方法,那麼繼承了接口b後的接口a也應該遵循上述原則:不該該包含用戶不使用的方法。 反之,則說明接口a被b給污染了,應該從新設計它們的關係。
若是用戶被迫依賴他們不使用的接口,當接口發生改變時,他們也不得不跟着改變。換而言之,一個用戶依賴了未使用但被其餘用戶使用的接口,當其餘用戶修改該接口時,依賴該接口的全部用戶都將受到影響。這顯然違反了開閉原則,也不是咱們所指望的。
下面咱們舉例說明怎麼設計接口或類之間的關係,使其不違反ISP原則。
假若有一個Door,有lock,unlock功能,另外,能夠在Door上安裝一個Alarm而使其具備報警功能。用戶能夠選擇通常的Door,也能夠選擇具備報警功能的Door。
有如下幾種設計方法:
ISP原則的違反例:
方法一:
在Door接口裏定義全部的方法。圖:
但這樣一來,依賴Door接口的CommonDoor卻不得不實現未使用的alarm()方法。違反了ISP原則。
方法二:
在Alarm接口定義alarm方法,在Door接口定義lock,unlock方法,Door接口繼承Alarm接口。
跟方法一同樣,依賴Door接口的CommonDoor卻不得不實現未使用的alarm()方法。違反了ISP原則。
遵循ISP原則的例:
方法三:經過多重繼承實現
Adapter設計模式的實現。
第2)種方案更具備實用性。
這種設計遵循了ISP設計原則。
方法四:經過委託實現
在Alarm接口定義alarm方法,在Door接口定義lock,unlock方法。接口之間無繼承關係。CommonDoor實現Door接口,
AlarmDoor有2種實現方案:
1)同時實現Door和Alarm接口。
2)繼承CommonDoor,並實現Alarm接口。該方案是繼承方式的
小結
四. Single Responsibility Principle (SRP) - 單一職責原則
永遠不要讓一個類存在多個改變的理由。換句話說,若是一個類須要改變,改變它的理由永遠只有一個。若是存在多個改變它的理由,就須要從新設計該類。
SRP(Single Responsibility Principle)原則的核心含意是:只能讓一個類有且僅有一個職責。這也是單一職責原則的命名含義。
爲何一個類不能有多於一個以上的職責呢?
若是一個類具備一個以上的職責,那麼就會有多個不一樣的緣由引發該類變化,而這種變化將影響到該類不一樣職責的使用者(不一樣用戶):
1,一方面,若是一個職責使用了外部類庫,則使用另一個職責的用戶卻也不得不包含這個未被使用的外部類庫。
2,另外一方面,某個用戶因爲某緣由須要修改其中一個職責,另一個職責的用戶也將受到影響,他將不得不從新編譯和配置。這違反了設計的開閉原則,也不是咱們所指望的。
職責的劃分
interface Modem { public void dial(String pno); //撥號 public void hangup(); //掛斷 public void send(char c); //發送數據 public char recv(); //接收數據 }
咋一看,這是一個沒有任何問題的接口設計。但事實上,這個接口包含了2個職責:第一個是鏈接管理(dial, hangup);另外一個是數據通訊(send, recv)。不少狀況下,這2個職責沒有任何共通的部分,它們由於不一樣的理由而改變,被不一樣部分的程序調用。
因此它違反了SRP原則。
下面的類圖將它的2個不一樣職責分紅2個不一樣的接口,這樣至少可讓客戶端應用程序使用具備單一職責的接口:
讓ModemImplementation實現這兩個接口。咱們注意到,ModemImplementation又組合了2個職責,這不是咱們但願的,但有時這又是必須的。一般因爲某些緣由,迫使咱們不得不綁定多個職責到一個類中,但咱們至少能夠經過接口的分割來分離應用程序關心的概念。
事實上,這個例子一個更好的設計應該是這樣的,如圖:
小結
五. The Open-Closed Principle (OCP) - 開閉原則
開閉原則(OCP:Open-Closed Principle)是指在進行面向對象設計(OOD:Object Oriented Design)中,設計類或其餘程序單位時,應該遵循:
- 對擴展開放(open)
- 對修改關閉(closed)
開閉原則是判斷面向對象設計是否正確的最基本的原理之一。 根據開閉原則,在設計一個軟件系統模塊(類,方法)的時候,應該能夠在不修改原有的模塊(修改關閉)的基礎上,能擴展其功能(擴展開放)。
A 擴展開放:某模塊的功能是可擴展的,則該模塊是擴展開放的。軟件系統的功能上的可擴展性要求模塊是擴展開放的。
B 修改關閉:某模塊被其餘模塊調用,若是該模塊的源代碼不容許修改,則該模塊修改關閉的。軟件系統的功能上的穩定性,持續性要求是修改關閉的。
這也是系統設計須要遵循開閉原則的緣由:
1)穩定性。開閉原則要求擴展功能不修改原來的代碼,這可讓軟件系統在變化中保持穩定。
2)擴展性。開閉原則要求對擴展開放,經過擴展提供新的或改變原有的功能,讓軟件系統具備靈活的可擴展性。
遵循開閉原則的系統設計,可讓軟件系統可複用,而且易於維護。
開閉原則的實現方法
爲了知足開閉原則的 對修改關閉(closed for modification) 原則以及擴展開放(open for extension) 原則,應該對軟件系統中的不變的部分加以抽象,在面向對象的設計中,
A 能夠把這些不變的部分加以抽象成不變的接口,這些不變的接口能夠應對將來的擴展;
B 接口的最小功能設計原則。根據這個原則,原有的接口要麼能夠應對將來的擴展;不足的部分能夠經過定義新的接口來實現;
C 模塊之間的調用經過抽象接口進行,這樣即便實現層發生變化,也無需修改調用方的代碼。
接口能夠被複用,但接口的實現卻不必定能被複用。接口是穩定的,關閉的,但接口的實現是可變的,開放的。能夠經過對接口的不一樣實現以及類的繼承行爲等爲系統增長新的或改變系統原來的功能,實現軟件系統的柔軟擴展。
簡單地說,軟件系統是否有良好的接口(抽象)設計是判斷軟件系統是否知足開閉原則的一種重要的判斷基準。如今多把開閉原則等同於面向接口的軟件設計。
開閉原則的相對性