Java六大設計原則

類的設計原則   


  1. 依賴倒置原則-Dependency Inversion Principle (DIP) 
  2. 里氏替換原則-Liskov Substitution Principle (LSP) 
  3. 接口分隔原則-Interface Segregation Principle (ISP) 
  4. 單一職責原則-Single Responsibility Principle (SRP) 
  5. 開閉原則-The Open-Closed Principle (OCP)

一. 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,即便對KeyboardPrint的封裝已經作得很是好,但若是Copy模塊裏直接使用KeyboardPrintCopy任很難被其餘應用環境(好比須要輸出到磁盤時)重用。 繼承

問題的解決: 接口

爲了解決上述問題,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/else之類對子類類型進行判斷的條件。如下代碼就違反LSP定義。


if (obj typeof Class1) {
     do something
} else if (obj typeof Class2) {
     do something else
}
子類應當能夠替換父類並出如今父類可以出現的任何地方,或者說若是咱們把代碼中使用基類的地方用它的子類所代替,代碼還能正常工做。

里氏替換原則LSP是使代碼符合開閉原則的一個重要保證。同時LSP體現了:
1) 類的繼承原則:若是一個繼承類的對象可能會在基類出現地方出現運行錯誤,則該子類不該該從該基類繼承,或者說,應該從新設計它們之間的關係。
2)動做正確性保證:從另外一個側面上保證了符合LSP設計原則的類的擴展不會給已有的系統引入新的錯誤。

類的繼承原則:
Robert C. Martin舉了Rectangle和Square的例子。這裏沿用這個例子,但用Java語言對其加以重寫,並忽略了某些細節只列出下面的精要部分來講明 里氏替換原則 對類的繼承上的約束。
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

         根據Bertrand Meyer提出的Design by Contract(DBC:基於合同的設計)概念的描述,對於類的一個方法,都有一個前提條件以及一個後續條件,前提條件說明方法接受什麼樣的參數數據等,只有前提條件獲得知足時,這個方法才能被調用;同時後續條件用來講明這個方法完成時的狀態,若是一個方法的執行會致使這個方法的後續條件不成立,那麼此方法也不該該正常返回。
         如今把前提條件以及後續條件應用到繼承子類中,子類方法應該知足:
1)前提條件不強於基類.
2)後續條件不弱於基類.
        換句話說,經過基類的接口調用一個對象時,用戶只知道基類前提條件以及後續條件。所以繼承類不得要求用戶提供比基類方法要求的更強的前提條件,亦即,繼承類方法必須接受任何基類方法能接受的任何條件(參數)。一樣,繼承類必須順從基類的全部後續條件,亦即,繼承類方法的行爲和輸出不得違反由基類創建起來的任何約束,不能讓用戶對繼承類方法的輸出感到困惑。這樣,咱們就有了基於合同的LSP,基於合同的LSP是LSP的一種強化。

       在不少狀況下,在設計初期咱們類之間的關係不是很明確,LSP則給了咱們一個判斷和設計類之間關係的基準:需不須要繼承,以及怎樣設計繼承關係。



三. 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接口定義lockunlock方法,Door接口繼承Alarm接口。


跟方法一同樣,依賴Door接口的CommonDoor卻不得不實現未使用的alarm()方法。違反了ISP原則。

遵循ISP原則的例:


方法三:經過多重繼承實現

Adapter設計模式的實現。
2)種方案更具備實用性。
這種設計遵循了ISP設計原則。

方法四:經過委託實現

Alarm接口定義alarm方法,在Door接口定義lockunlock方法。接口之間無繼承關係。CommonDoor實現Door接口,
AlarmDoor
2種實現方案:
1
)同時實現DoorAlarm接口。
2
)繼承CommonDoor,並實現Alarm接口。該方案是繼承方式的

小結

Interface Segregation Principle (ISP)從對接口的使用上爲咱們對接口抽象的顆粒度創建了判斷基準:在爲系統設計接口的時候,使用多個專門的接口代替單一的胖接口。




四. Single Responsibility Principle (SRP) - 單一職責原則


    永遠不要讓一個類存在多個改變的理由。換句話說,若是一個類須要改變,改變它的理由永遠只有一個。若是存在多個改變它的理由,就須要從新設計該類。

SRP
Single Responsibility Principle)原則的核心含意是:只能讓一個類有且僅有一個職責。這也是單一職責原則的命名含義。

爲何一個類不能有多於一個以上的職責呢?
若是一個類具備一個以上的職責,那麼就會有多個不一樣的緣由引發該類變化,而這種變化將影響到該類不一樣職責的使用者(不一樣用戶):
1
,一方面,若是一個職責使用了外部類庫,則使用另一個職責的用戶卻也不得不包含這個未被使用的外部類庫。
2
,另外一方面,某個用戶因爲某緣由須要修改其中一個職責,另一個職責的用戶也將受到影響,他將不得不從新編譯和配置。這違反了設計的開閉原則,也不是咱們所指望的。

職責的劃分

既然一個類不能有多個職責,那麼怎麼劃分職責呢?
Robert.C Martin給出了一個著名的定義:所謂一個類的一個職責是指引發該類變化的一個緣由。若是你能想到一個類存在多個使其改變的緣由,那麼這個類就存在多個職責。

Single Responsibility Principle (SRP)的原文裏舉了一個Modem的例子來講明怎麼樣進行職責的劃分,這裏咱們也沿用這個例子來講明一下:

SRP違反例:
Modem.java
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個職責,這不是咱們但願的,但有時這又是必須的。一般因爲某些緣由,迫使咱們不得不綁定多個職責到一個類中,但咱們至少能夠經過接口的分割來分離應用程序關心的概念。
事實上,這個例子一個更好的設計應該是這樣的,如圖:

小結

Single Responsibility Principle (SRP)從職責(改變理由)的側面上爲咱們對類(接口)的抽象的顆粒度創建了判斷基準:在爲系統設計類(接口)的時候應該保證它們的單一職責性。


五. The Open-Closed Principle (OCP) - 開閉原則

開閉原則(OCPOpen-Closed Principle)是指在進行面向對象設計(OODObject Oriented Design)中,設計類或其餘程序單位時,應該遵循:
-
對擴展開放(open
-
對修改關閉(closed
 
開閉原則是判斷面向對象設計是否正確的最基本的原理之一。 根據開閉原則,在設計一個軟件系統模塊(類,方法)的時候,應該能夠在不修改原有的模塊(修改關閉)的基礎上,能擴展其功能(擴展開放)。
擴展開放:某模塊的功能是可擴展的,則該模塊是擴展開放的。軟件系統的功能上的可擴展性要求模塊是擴展開放的。
 B
修改關閉:某模塊被其餘模塊調用,若是該模塊的源代碼不容許修改,則該模塊修改關閉的。軟件系統的功能上的穩定性,持續性要求是修改關閉的。

這也是系統設計須要遵循開閉原則的緣由
1
)穩定性。開閉原則要求擴展功能不修改原來的代碼,這可讓軟件系統在變化中保持穩定。
2
)擴展性。開閉原則要求對擴展開放,經過擴展提供新的或改變原有的功能,讓軟件系統具備靈活的可擴展性。
遵循開閉原則的系統設計,可讓軟件系統可複用,而且易於維護。

開閉原則的實現方法

爲了知足開閉原則的 對修改關閉(closed for modification) 原則以及擴展開放(open for extension) 原則,應該對軟件系統中的不變的部分加以抽象,在面向對象的設計中,
A
能夠把這些不變的部分加以抽象成不變的接口,這些不變的接口能夠應對將來的擴展;
B
接口的最小功能設計原則。根據這個原則,原有的接口要麼能夠應對將來的擴展;不足的部分能夠經過定義新的接口來實現;
C
模塊之間的調用經過抽象接口進行,這樣即便實現層發生變化,也無需修改調用方的代碼。

接口能夠被複用,但接口的實現卻不必定能被複用。接口是穩定的,關閉的,但接口的實現是可變的,開放的。能夠經過對接口的不一樣實現以及類的繼承行爲等爲系統增長新的或改變系統原來的功能,實現軟件系統的柔軟擴展。

        
簡單地說,軟件系統是否有良好的接口(抽象)設計是判斷軟件系統是否知足開閉原則的一種重要的判斷基準。如今多把開閉原則等同於面向接口的軟件設計。

開閉原則的相對性

軟件系統的構建是一個須要不斷重構的過程,在這個過程當中,模塊的功能抽象,模塊與模塊間的關係,都不會從一開始就很是清晰明瞭,因此構建100%知足開閉原則的軟件系統是至關困難的,這就是開閉原則的相對性。但在設計過程當中,經過對模塊功能的抽象(接口定義),模塊之間的關係的抽象(經過接口調用),抽象與實現的分離(面向接口的程序設計)等,能夠儘可能接近知足開閉原則。
相關文章
相關標籤/搜索