天天看點

Java架構師必須知道的 6 大設計原則

開發原則

面向對象的基本原則(solid)是五個,但是在經常被提到的除了這五個之外還有 迪米特法則和合成複用原則等, 是以在常見的文章中有表示寫六大或七大原則的; 除此之外我還将給出一些其它相關書籍和網際網路上出現的原則;

S單一職責SRP

Single-Responsibility Principle, 一個類,最好隻做一件事,隻有一個引起它的變化。單一職責原則可以看做是低耦合,高内聚在面向對象原則的引申,将職責定義為引起變化的原因,以提高内聚性減少引起變化的原因。

####

定義

一個對象應該隻包含單一的職責,并且該職責被完整地封裝在一個類中。(Every object should have a single responsibility, and that responsibility should be entirely encapsulated  by the class.),即又定義有且僅有一個原因使類變更。點選這裡檢視高可用架構設計9種方案詳解。

#####

原則分析

一個類(或者大到子產品,小到方法)承擔的職責越多,它被複用的可能性越小,而且如果一個類承擔的職責過多,就相當于将這些職責耦合在一起,當其中一個職責變化時,可能會影響其他職責的運作。

類的職責主要包括兩個方面:資料職責和行為職責,資料職責通過其屬性來展現,而行為職責通過其方法來展現。

單一職責原則是實作高内聚、低耦合的指導方針,在很多代碼重構手法中都能找到它的存在,它是最簡單但又最難運用的原則,需要設計人員發現類的不同職責并将其分離,而發現類的多重職責需要設計人員具有較強的分析設計能力和相關重構經驗。

優點

降低類的複雜性,類的職責清晰明确。比如資料職責和行為職責清晰明确。

提高類的可讀性和維護性,

變更引起的風險減低,變更是必不可少的,如果接口的單一職責做得好,一個接口修改隻對相應的類有影響,對其他接口無影響,這對系統的擴充性、維護性都有非常大的幫助。

注意:單一職責原則提出了一個編寫程式的标準,用“職責”或“變化原因”來衡量接口或類設計得是否合理,但是“職責”和“變化原因”都是沒有具體标準的,一個類到底要負責那些職責?這些職責怎麼細化?細化後是否都要有一個接口或類?這些都需從實際的情況考慮。因項目而異,因環境而異。

例子

SpringMVC 中Entity,DAO,Service,Controller, Util等的分離。

O開放封閉原則OCP

Open - ClosedPrinciple ,OCP, 對擴充開放,對修改關閉(設計模式的核心原則)

一個軟體實體(如類、子產品和函數)應該對擴充開放,對修改關閉。意思是,在一個系統或者子產品中,對于擴充是開放的,對于修改是關閉的,一個 好的系統是在不修改源代碼的情況下,可以擴充你的功能。而實作開閉原則的關鍵就是抽象化。

當軟體實體因需求要變化時, 盡量通過擴充已有軟體實體,可以提供新的行為,以滿足對軟體的新的需求,而不是修改已有的代碼,使變化中的軟體有一定的适應性和靈活性 。已有軟體子產品,特别是最重要的抽象層子產品不能再修改,這使變化中的軟體系統有一定的穩定性和延續性。

實作開閉原則的關鍵就是抽象化 :在"開-閉"原則中,不允許修改的是抽象的類或者接口,允許擴充的是具體的實作類,抽象類和接口在"開-閉"原則中扮演着極其重要的角色..即要預知可能變化的需求.又預見所有可能已知的擴充..是以在這裡"抽象化"是關鍵!

可變性的封閉原則:找到系統的可變因素,将它封裝起來. 這是對"開-閉"原則最好的實作. 不要把你的可變因素放在多個類中,或者散落在程式的各個角落. 你應該将可變的因素,封套起來..并且切忌不要把所用的可變因素封套在一起. 最好的解決辦法是,分塊封套你的可變因素!避免超大類,超長類,超長方法的出現!!給你的程式增加藝術氣息,将程式藝術化是我們的目标!

設計模式中模闆方法模式和觀察者模式都是開閉原則的極好展現。點選這裡檢視高可用架構設計9種方案詳解。

L裡氏替換原則LSP

Liskov Substitution Principle,LSP:任何基類可以出現的地方,子類也可以出現;這一思想表現為對繼承機制的限制規範,隻有子類能夠替換其基類時,才能夠保證系統在運作期内識别子類,這是保證繼承複用的基礎。

第一種定義方式相對嚴格:如果對每一個類型為S的對象o1,都有類型為T的對象o2,使得以T定義的所有程式P在所有的對象o1都代換成o2時,程式P的行為沒有變化,那麼類型S是類型T的子類型。

第二種更容易了解的定義方式:所有引用基類(父類)的地方必須能透明地使用其子類的對象。即子類能夠必須能夠替換基類能夠從出現的地方。子類也能在基類 的基礎上新增行為。

(裡氏代換原則由2008年圖靈獎得主、美國第一位計算機科學女博士、麻省理工學院教授BarbaraLiskov和卡内基.梅隆大學Jeannette Wing教授于1994年提出。其原文如下:Let q(x) be a property provableabout objects x of type T. Then q(y) should be true for objects y of type Swhere S is a subtype of T. )

講的是基類和子類的關系,隻有這種關系存在時,裡氏代換原則才存在。正方形是長方形是了解裡氏代換原則的經典例子。

裡氏代換原則可以通俗表述為:在軟體中如果能夠使用基類對象,那麼一定能夠使用其子類對象。把基類都替換成它的子類,程式将不會産生任何錯誤和異常,反過來則不成立,如果一個軟體實體使用的是一個子類的話,那麼它不一定能夠使用基類。

裡氏代換原則是實作開閉原則的重要方式之一,由于使用基類對象的地方都可以使用子類對象,是以在程式中盡量使用基類類型來對對象進行定義,而在運作時再确定其子類類型,用子類對象來替換父類對象。

I接口隔離法則

(Interface Segregation Principle,ISL):用戶端不應該依賴那些它不需要的接口。(這個法則與迪米特法則是相通的)

用戶端不應該依賴那些它不需要的接口。

另一種定義方法:一旦一個接口太大,則需要将它分割成一些更細小的接口,使用該接口的用戶端僅需知道與之相關的方法即可。 注意,在該定義中的接口指的是所定義的方法。例如外面調用某個類的public方法。這個方法對外就是接口。

1)接口隔離原則是指使用多個專門的接口,而不使用單一的總接口。每一個接口應該承擔一種相對獨立的角色,不多不少,不幹不該幹的事,該幹的事都要幹。

一個接口就隻代表一個角色,每個角色都有它特定的一個接口,此時這個原則可以叫做“角色隔離原則”。

接口僅僅提供用戶端需要的行為,即所需的方法,用戶端不需要的行為則隐藏起來,應當為用戶端提供盡可能小的單獨的接口,而不要提供大的總接口。

2)使用接口隔離原則拆分接口時,首先必須滿足單一職責原則,将一組相關的操作定義在一個接口中,且在滿足高内聚的前提下,接口中的方法越少越好。

3)可以在進行系統設計時采用定制服務的方式,即為不同的用戶端提供寬窄不同的接口,隻提供使用者需要的行為,而隐藏使用者不需要的行為。

D依賴倒置原則DIP

Dependency-Inversion Principle 要依賴抽象,而不要依賴具體的實作,具體而言就是高層子產品不依賴于底層子產品,二者共同依賴于抽象。抽象不依賴于具體,具體依賴于抽象。

高層子產品不應該依賴低層子產品,它們都應該依賴抽象。抽象不應該依賴于細節,細節應該依賴于抽象。簡單的說,依賴倒置原則要求用戶端依賴于抽象耦合。原則表述:

1)抽象不應當依賴于細節;細節應當依賴于抽象;

2)要針對接口程式設計,不針對實作程式設計。

1)如果說開閉原則是面向對象設計的目标,依賴倒轉原則是到達面向設計"開閉"原則的手段。如果要達到最好的"開閉"原則,就要盡量的遵守依賴倒轉原則。可以說依賴倒轉原則是對“抽象化”的最好規範!我個人感覺,依賴倒轉原則也是裡氏代換原則的補充。你了解了裡氏代換原則,再來了解依賴倒轉原則應該是很容易的。

2)依賴倒轉原則的常用實作方式之一是在代碼中使用抽象類,而将具體類放在配置檔案中。

3)類之間的耦合:零耦合關系,具體耦合關系,抽象耦合關系。依賴倒轉原則要求用戶端依賴于抽象耦合,以抽象方式耦合是依賴倒轉原則的關鍵。

了解這個依賴倒置,首先我們需要明白依賴在面向對象設計的概念:

依賴關系(Dependency):是一種使用關系,特定事物的改變有可能會影響到使用該事物的其他事物,在需要表示一個事物使用另一個事物時使用依賴關系。(假設A類的變化引起了B類的變化,則說名B類依賴于A類。)大多數情況下,依賴關系展現在某個類的方法使用另一個類的對象作為參數。在UML中,依賴關系用帶箭頭的虛線表示,由依賴的一方指向被依賴的一方。

例子:某系統提供一個資料轉換子產品,可以将來自不同資料源的資料轉換成多種格式,如可以轉換來自資料庫的資料(DatabaseSource)、也可以轉換來自文本檔案的資料(TextSource),轉換後的格式可以是XML檔案(XMLTransformer)、也可以是XLS檔案(XLSTransformer)等。

Java架構師必須知道的 6 大設計原則
由于需求的變化,該系統可能需要增加新的資料源或者新的檔案格式,每增加一個新的類型的資料源或者新的類型的檔案格式,客戶類MainClass都需要修改源代碼,以便使用新的類,但違背了開閉原則。現使用依賴倒轉原則對其進行重構。點選這裡檢視高可用架構設計9種方案詳解。
Java架構師必須知道的 6 大設計原則
當然根據具體的情況,也可以将AbstractSource注入到AbstractStransformer,依賴注入的方式有以下三種:

/**
 * 依賴注入是依賴AbstractSource抽象注入的,而不是具體
 * DatabaseSource
 *
 */
abstract class AbstractStransformer {
    private AbstractSource source;
    /**
     * 構造注入(Constructor Injection):通過構造函數注入執行個體變量。
     */
    public void AbstractStransformer(AbstractSource source){
        this.source = source;
    }
    /**
     * 設值注入(Setter Injection):通過Setter方法注入執行個體變量。
     * @param source : the sourceto set
     */
    public void setSource(AbstractSource source) {
        this.source = source;
    }
    /**
     * 接口注入(Interface Injection):通過接口方法注入執行個體變量。
     * @param source
     */
    public void transform(AbstractSource source ) {
        source.getSource();
        System.out.println("Stransforming ...");
    }
}      

###

合成/聚合複用原則

(Composite/Aggregate ReusePrinciple ,CARP):要盡量使用對象組合,而不是繼承關系達到軟體複用的目的

經常又叫做合成複用原則(Composite ReusePrinciple或CRP),盡量使用對象組合,而不是繼承來達到複用的目的。

就是在一個新的對象裡面使用一些已有的對象,使之成為新對象的一部分;新對象通過向這些對象的委派達到複用已有功能的目的。簡而言之,要盡量使用合成/聚合,盡量不要使用繼承。

1)在面向對象設計中,可以通過兩種基本方法在不同的環境中複用已有的設計和實作,即通過組合/聚合關系或通過繼承。

繼承複用:實作簡單,易于擴充。破壞系統的封裝性;從基類繼承而來的實作是靜态的,不可能在運作時發生改變,沒有足夠的靈活性;隻能在有限的環境中使用。(“白箱”複用)

組合/聚合複用:耦合度相對較低,選擇性地調用成員對象的操作;可以在運作時動态進行。(“黑箱”複用)

2)組合/聚合可以使系統更加靈活,類與類之間的耦合度降低,一個類的變化對其他類造成的影響相對較少,是以一般首選使用組合/聚合來實作複用;其次才考慮繼承,在使用繼承時,需要嚴格遵循裡氏代換原則,有效使用繼承會有助于對問題的了解,降低複雜度,而濫用繼承反而會增加系統建構和維護的難度以及系統的複雜度,是以需要慎重使用繼承複用。

3)此原則和裡氏代換原則氏相輔相成的,兩者都是具體實作"開-閉"原則的規範。違反這一原則,就無法實作"開-閉"原則,首先我們要明白合成和聚合的概念:

注意:聚合群組合的差別是什麼?

合成(組合):表示一個整體與部分的關系,指一個依托整體而存在的關系(整體與部分不可以分開);比如眼睛和嘴對于頭來說就是組合關系,沒有了頭就沒有眼睛和嘴,它們是不可分割的。在UML中,組合關系用帶實心菱形的直線表示。

聚合:聚合是比合成關系的一種更強的依賴關系,也表示整體與部分的關系(整體與部分可以分開);比如螺絲和汽車玩具的關系,螺絲脫離玩具依然可以用在其它裝置之上。在UML中,聚合關系用帶空心菱形的直線表示。

迪米特法則

(Law of Demeter,LoD:系統中的類,盡量不要與其他類互相作用,減少類之間的耦合度

又叫最少知識原則(Least Knowledge Principle或簡寫為LKP)幾種形式定義:

不要和“陌生人”說話。英文定義為:Don't talk to strangers.

隻與你的直接朋友通信。英文定義為:Talk only to your immediate friends.

每一個軟體機關對其他的機關都隻有最少的知識,而且局限于那些與本機關密切相關的軟體機關。

簡單地說,也就是,一個對象應當對其它對象有盡可能少的了解。一個類應該對自己需要耦合或調用的類知道得最少,你(被耦合或調用的類)的内部是如何複雜都和我沒關系,那是你的事情,我就知道你提供的public方法,我就調用這麼多,其他的一概不關心。

法則分析

朋友類:

在迪米特法則中,對于一個對象,其朋友包括以下幾類: (1) 目前對象本身(this); (2) 以參數形式傳入到目前對象方法中的對象; (3) 目前對象的成員對象; (4) 如果目前對象的成員對象是一個集合,那麼集合中的元素也都是朋友;(5) 目前對象所建立的對象。

任何一個對象,如果滿足上面的條件之一,就是目前對象的“朋友”,否則就是“陌生人”。

狹義法則和廣義法則:

在狹義的迪米特法則中,如果兩個類之間不必彼此直接通信,那麼這兩個類就不應當發生直接的互相作用,如果其中的一個類需要調用另一個類的某一個方法的話,可以通過第三者轉發這個調用。

狹義的迪米特法則:可以降低類之間的耦合,但是會在系統中增加大量的小方法并散落在系統的各個角落,它可以使一個系統的局部設計簡化,因為每一個局部都不會和遠距離的對象有直接的關聯,但是也會造成系統的不同子產品之間的通信效率降低,使得系統的不同子產品之間不容易協調。點選這裡檢視高可用架構設計9種方案詳解。

廣義的迪米特法則:指對對象之間的資訊流量、流向以及資訊的影響的控制,主要是對資訊隐藏的控制。資訊的隐藏可以使各個子系統之間脫耦,進而允許它們獨立地被開發、優化、使用和修改,同時可以促進軟體的複用,由于每一個子產品都不依賴于其他子產品而存在,是以每一個子產品都可以獨立地在其他的地方使用。一個系統的規模越大,資訊的隐藏就越重要,而資訊隐藏的重要性也就越明顯。

迪米特法則的主要用途:在于控制資訊的過載。

在類的劃分上,應當盡量建立松耦合的類,類之間的耦合度越低,就越有利于複用,一個處在松耦合中的類一旦被修改,不會對關聯的類造成太大波及;

在類的結構設計上,每一個類都應當盡量降低其成員變量和成員函數的通路權限;

在類的設計上,隻要有可能,一個類型應當設計成不變類;

在對其他類的引用上,一個對象對其他對象的引用應當降到最低。

外觀模式Facade(結構型)

迪米特法則與設計模式Facade模式、Mediator模式

系統中的類,盡量不要與其他類互相作用,減少類之間的耦合度,因為在你的系統中,擴充的時候,你可能需要修改這些類,而類與類之間的關系,決定了修改的複雜度,互相作用越多,則修改難度就越大,反之,如果互相作用的越小,則修改起來的難度就越小。例如A類依賴B類,則B類依賴C類,當你在修改A類的時候,你要考慮B類是否會受到影響,而B類的影響是否又會影響到C類。 如果此時C類再依賴D類的話,呵呵,我想這樣的修改有的受了。

Q&A

面向對象設計其他原則

封裝變化

少用繼承 多用組合

針對接口程式設計 不針對實作程式設計

為互動對象之間的松耦合設計而努力

類應該對擴充開發 對修改封閉(開閉OCP原則)

依賴抽象,不要依賴于具體類(依賴倒置DIP原則)

密友原則:隻和朋友交談(最少知識原則,迪米特法則)

說明:一個對象應當對其他對象有盡可能少的了解,将方法調用保持在界限内,隻調用屬于以下範圍的方法: 該對象本身(本地方法)對象的元件 被當作方法參數傳進來的對象 此方法建立或執行個體化的任何對象

别找我(調用我) 我會找你(調用你)(好萊塢原則)

一個類隻有一個引起它變化的原因(單一職責SRP原則)

你能解釋一下裡氏替換原則嗎?

嚴格定義:如果對每一個類型為S的對象o1,都有類型為T的對象o2,使得以T定義的所有程式P在所有的對象用o1替換o2時,程式P的行為沒有變化,那麼類型S是類型T的子類型。

通俗表述:所有引用基類(父類)的地方必須能透明地使用其子類的對象。也就是說子類可以擴充父類的功能,但不能改變父類原有的功能。它包含以下4層含義:

子類可以實作父類的抽象方法,但不能覆寫父類的非抽象方法。

子類中可以增加自己特有的方法。

當子類的方法重載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬松。

當子類的方法實作父類的抽象方法時,方法的後置條件(即方法的傳回值)要比父類更嚴格。

什麼情況下會違反迪米特法則?為什麼會有這個問題?

迪米特法則建議“隻和朋友說話,不要陌生人說話”,以此來減少類之間的耦合。

給我一個符合開閉原則的設計模式的例子?

開閉原則要求你的代碼對擴充開放,對修改關閉。這個意思就是說,如果你想增加一個新的功能,你可以很容易的在不改變已測試過的代碼的前提下增加新的代碼。有好幾個設計模式是基于開閉原則的,如政策模式,如果你需要一個新的政策,隻需要實作接口,增加配置,不需要改變核心邏輯。一個正在工作的例子是 Collections.sort() 方法,這就是基于政策模式,遵循開閉原則的,你不需為新的對象修改 sort() 方法,你需要做的僅僅是實作你自己的 Comparator 接口。

什麼時候使用享元模式(蠅量模式)?

享元模式通過共享對象來避免建立太多的對象。為了使用享元模式,你需要確定你的對象是不可變的,這樣你才能安全的共享。JDK 中 String 池、Integer 池以及 Long 池都是很好的使用了享元模式的例子。