我們在應用程序開發中,一般要求儘量兩做到可維護性和可複用性。
應用程序的複用可以提高應用程序的開發效率和質量,節約開發成本,恰當的複用還可以改善系統的可維護性。而在面向對象的設計裏面,可維護性複用都是以面向對象設計原則爲基礎的,這些設計原則首先都是複用的原則,遵循這些設計原則可以有效地提高系統的複用性,同時提高系統的可維護性。 面向對象設計原則和設計模式也是對系統進行合理重構的指導方針。
常用的面向對象設計原則包括7個,這些原則並不是孤立存在的,它們相互依賴,相互補充。
1.單一職責原則(Single Responsibility Principle,SRP):類的職責要單一,不能將太多的職責放在一個類中。(高內聚、低耦合)
定義:一個對象應該只包含單一的職責,並且該職責被完整地封裝在一個類中。(Every object should have a single responsibility, and that responsibility should be entirely encapsulated by the class.),即又定義有且僅有一個原因使類變更。
原則分析:1)一個類(或者大到模塊,小到方法)承擔的職責越多,它被複用的可能性越小,而且如果一個類承擔的職責過多,就相當於將這些職責耦合在一起,當其中一個職責變化時,可能會影響其他職責的運作。
2)類的職責主要包括兩個方面:數據職責和行爲職責,數據職責通過其屬性來體現,而行爲職責通過其方法來體現。
3)單一職責原則是實現高內聚、低耦合的指導方針,在很多代碼重構手法中都能找到它的存在,它是最簡單但又最難運用的原則,需要設計人員發現類的不同職責並將其分離,而發現類的多重職責需要設計人員具有較強的分析設計能力和相關重構經驗。例子:最簡單例子是:一個數據結構職責類和算法行爲都放在一個類User。我們應該把數據結構和行爲分開。現使用單一職責原則對User類進行重構。優點:
1、降低類的複雜性,類的職責清晰明確。比如數據職責和行爲職責清晰明確。
2、提高類的可讀性和維護性,
4、變更引起的風險減低,變更是必不可少的,如果接口的單一職責做得好,一個接口修改只對相應的類有影響,對其他接口無影響,這對系統的擴展性、維護性都有非常大的幫助。
注意:單一職責原則提出了一個編寫程序的標準,用“職責”或“變化原因”來衡量接口或類設計得是否合理,但是“職責”和“變化原因”都是沒有具體標準的,一個類到底要負責那些職責?這些職責怎麼細化?細化後是否都要有一個接口或類?這些都需從實際的情況考慮。因項目而異,因環境而異。
2.開閉原則( Open - ClosedPrinciple ,OCP ):對擴展開放,對修改關閉(設計模式的核心原則是)
定義:一個軟件實體(如類、模塊和函數)應該對擴展開放,對修改關閉. 意思是,在一個系統或者模塊中,對於擴展是開放的,對於修改是關閉的,一個 好的系統是在不修改源代碼的情況下,可以擴展你的功能. 而實現開閉原則的關鍵就是抽象化.
原則分析 :
1)當軟件實體因需求要變化時, 儘量通過擴展已有軟件實體,可以提供新的行爲,以滿足對軟件的新的需求,而不是修改已有的代碼,使變化中的軟件有一定的適應性和靈活性 。已有軟件模塊,特別是最重要的抽象層模塊不能再修改,這使變化中的軟件系統有一定的穩定性和延續性。
2)實現開閉原則的關鍵就是抽象化 :在"開-閉"原則中,不允許修改的是抽象的類或者接口,允許擴展的是具體的實現類,抽象類和接口在"開-閉"原則中扮演着極其重要的角色..即要預知可能變化的需求.又預見所有可能已知的擴展..所以在這裏"抽象化"是關鍵!
3)可變性的封閉原則:找到系統的可變因素,將它封裝起來. 這是對"開-閉"原則最好的實現. 不要把你的可變因素放在多個類中,或者散落在程序的各個角落. 你應該將可變的因素,封套起來..並且切忌不要把所用的可變因素封套在一起. 最好的解決辦法是,分塊封套你的可變因素!避免超大類,超長類,超長方法的出現!!給你的程序增加藝術氣息,將程序藝術化是我們的目標!
例子:我們前面提到的模板方法模式和觀察者模式都是開閉原則的極好體現。
定義:第一種定義方式相對嚴格:如果對每一個類型爲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. )原則分析:1)講的是基類和子類的關係,只有這種關係存在時,里氏代換原則才存在。正方形是長方形是理解里氏代換原則的經典例子。2)里氏代換原則可以通俗表述爲:在軟件中如果能夠使用基類對象,那麼一定能夠使用其子類對象。把基類都替換成它的子類,程序將不會產生任何錯誤和異常,反過來則不成立,如果一個軟件實體使用的是一個子類的話,那麼它不一定能夠使用基類。3)里氏代換原則是實現開閉原則的重要方式之一,由於使用基類對象的地方都可以使用子類對象,因此在程序中儘量使用基類類型來對對象進行定義,而在運行時再確定其子類類型,用子類對象來替換父類對象。例子:正方形不是長方形 在數學領域裏,正方形毫無疑問是長方形,它是一個長寬相等的長方形。所以,我們開發的一個與幾何圖形相關的軟件系統中,讓正方形繼承自長方形是順利成章的事情。由於正方形的度和寬度必須相等,所以在方法setLength和setWidth中,對長度和寬度賦值相同。
- /**
- * 長方形類Rectangle:
- *
- */
- class Rectangle {
- double length;
- double width;
- public double getLength() { return length; }
- public void setLength(double height) { this.length = length; }
- public double getWidth() { return width; }
- public void setWidth(double width) { this.width = width; }
- }
正方形集成長方形:
- /**
- * 正方形類Square:
- */
- class Square extends Rectangle {
- public void setWidth(double width) {
- super.setLength(width);
- super.setWidth(width);
- }
- public void setLength(double length) {
- super.setLength(length);
- super.setWidth(length);
- }
- }
由於正方形的度和寬度必須相等,所以在方法setLength和setWidth中,對長度和寬度賦值相同。類TestRectangle是我們的軟件系統中的一個組件,它有一個resize方法要用到基類Rectangle,resize方法的功能是模擬長方形寬度逐步增長的效果:
測試類TestRectangle:
我們運行一下這段代碼就會發現,假如我們把一個普通長方形作爲參數傳入resize方法,就會看到長方形寬度逐漸增長的效果,當寬度大於長度,代碼就會停止,這種行爲的結果符合我們的預期;假如我們再把一個正方形作爲參數傳入resize方法後,就會看到正方形的寬度和長度都在不斷增長,代碼會一直運行下去,直至系統產生溢出錯誤。所以,普通的長方形是適合這段代碼的,正方形不適合。
- class TestRectangle {
- public void resize(Rectangle objRect) {
- while(objRect.getWidth() <= objRect.getLength() ) {
- objRect.setWidth( objRect.getWidth () + 1 );
- }
- }
- }
我們得出結論:在resize方法中,Rectangle類型的參數是不能被Square類型的參數所代替,如果進行了替換就得不到預期結果。因此,Square類和Rectangle類之間的繼承關係違反了里氏代換原則,它們之間的繼承關係不成立,正方形不是長方形。優缺點:在面向對象的語言中,繼承是必不可少的、非常優秀的語言機制,它有如下優點:1)代碼共享,減少創建類的工作量,每個子類都擁有父類的方法和屬性;2)提高代碼的重用性;
3)子類可以形似父類,但又異於父類,“龍生龍,鳳生鳳,老鼠生來會打洞”是說子擁有父的“種”,“世界上沒有兩片完全相同的葉子”是指明子與父的不同;
4)提高代碼的可擴展性,實現父類的方法就可以“爲所欲爲”了,君不見很多開源框架的擴展接口都是通過繼承父類來完成的;
5)提高產品或項目的開放性。
自然界的所有事物都是優點和缺點並存的,即使是雞蛋,有時候也能挑出骨頭來,繼承的缺點如下:
1)繼承是侵入性的。只要繼承,就必須擁有父類的所有屬性和方法;
2)降低代碼的靈活性。子類必須擁有父類的屬性和方法,讓子類自由的世界中多了些約束;
3)增強了耦合性。當父類的常量、變量和方法被修改時,必需要考慮子類的修改,而且在缺乏規範的環境下,這種修改可能帶來非常糟糕的結果——大片的代碼需要重構。
4.依賴倒轉原則( Dependence Inversion Principle ,DIP ):要依賴抽象,而不要依賴具體的實現.
定義:高層模塊不應該依賴低層模塊,它們都應該依賴抽象。抽象不應該依賴於細節,細節應該依賴於抽象。簡單的說,依賴倒置原則要求客戶端依賴於抽象耦合。原則表述:
1)抽象不應當依賴於細節;細節應當依賴於抽象;
2)要針對接口編程,不針對實現編程。
原則分析:
1)如果說開閉原則是面向對象設計的目標,依賴倒轉原則是到達面向設計"開閉"原則的手段..如果要達到最好的"開閉"原則,就要儘量的遵守依賴倒轉原則. 可以說依賴倒轉原則是對"抽象化"的最好規範! 我個人感覺,依賴倒轉原則也是里氏代換原則的補充..你理解了里氏代換原則,再來理解依賴倒轉原則應該是很容易的。
2)依賴倒轉原則的常用實現方式之一是在代碼中使用抽象類,而將具體類放在配置文件中。
3)類之間的耦合:零耦合關係,具體耦合關係,抽象耦合關係。依賴倒轉原則要求客戶端依賴於抽象耦合,以抽象方式耦合是依賴倒轉原則的關鍵。理解這個依賴倒置,首先我們需要明白依賴在面向對象設計的概念:依賴關係(Dependency):是一種使用關係,特定事物的改變有可能會影響到使用該事物的其他事物,在需要表示一個事物使用另一個事物時使用依賴關係。(假設A類的變化引起了B類的變化,則說名B類依賴於A類。)大多數情況下,依賴關係體現在某個類的方法使用另一個類的對象作爲參數。在UML中,依賴關係用帶箭頭的虛線表示,由依賴的一方指向被依賴的一方。例子:某系統提供一個數據轉換模塊,可以將來自不同數據源的數據轉換成多種格式,如可以轉換來自數據庫的數據(DatabaseSource)、也可以轉換來自文本文件的數據(TextSource),轉換後的格式可以是XML文件(XMLTransformer)、也可以是XLS文件(XLSTransformer)等。
由於需求的變化,該系統可能需要增加新的數據源或者新的文件格式,每增加一個新的類型的數據源或者新的類型的文件格式,客戶類MainClass都需要修改源代碼,以便使用新的類,但違背了開閉原則。現使用依賴倒轉原則對其進行重構。
- <pre name="code" class="java">/**
- * 抽象接口
- * @author mo-87
- *
- */
- abstract public class AbstractSource {
- abstract public void getSource();
- }
- /**
- * 抽象接口
- * @author mo-87
- *
- */
- abstract public class AbstractStransformer {
- abstract public void transform();
- }
- /**
- * 具體實現
- * @author mo-87
- *
- */
- public class DatabaseSource extends AbstractSource{
- public void getSource(){
- System.out.println("Get source data");
- }
- }
- /**
- * 依賴注入是依賴AbstractSource抽象注入的,而不是具體
- * DatabaseSource
- *
- * @author mo-87
- *
- */
- public class XMLStransformer {
- /**
- *
- */
- private AbstractSource source;
- /**
- * 構造注入(Constructor Injection):通過構造函數注入實例變量。
- */
- public void XMLStransformer(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 ...");
- }
- }</pre>
- <pre></pre>
- <pre></pre>
- <pre></pre>
依賴注入的三種寫法:•構造注入(Constructor Injection):通過構造函數注入實例變量。•設值注入(Setter Injection):通過Setter方法注入實例變量。•接口注入(Interface Injection):通過接口方法注入實例變量。
- public class XMLStransformer {
- /**
- *
- */
- private AbstractSource source;
- /**
- * 構造注入(Constructor Injection):通過構造函數注入實例變量。
- */
- public void XMLStransformer(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 ...");
- }
- }
優點:採用依賴倒置原則可以減少類間的耦合性,提高系統的穩定性,降低並行開發引起的風險,提高代碼的可讀性和可維護性。
依賴正置就是類間的依賴是實實在在的實現類間的依賴,也就是面向實現編程,這也是正常人的思維方式,我要開奔馳車就依賴奔馳車,我要使用筆記本電腦就直接依賴筆記本電腦,而編寫程序需要的是對現實世界的事物進行抽象,抽象的結構就是有了抽象類和接口,然後我們根據系統設計的需要產生了抽象間的依賴,代替了人們傳統思維中的事物間的依賴,“倒置”就是從這裏產生的。
5 .合成/聚合複用原則(Composite/Aggregate ReusePrinciple ,CARP):要儘量使用對象組合,而不是繼承關係達到軟件複用的目的
定義:經常又叫做合成複用原則(Composite ReusePrinciple或CRP),儘量使用對象組合,而不是繼承來達到複用的目的。
就是在一個新的對象裏面使用一些已有的對象,使之成爲新對象的一部分;新對象通過向這些對象的委派達到複用已有功能的目的。簡而言之,要儘量使用合成/聚合,儘量不要使用繼承。
原則分析:
1)在面向對象設計中,可以通過兩種基本方法在不同的環境中複用已有的設計和實現,即通過組合/聚合關係或通過繼承。繼承複用:實現簡單,易於擴展。破壞系統的封裝性;從基類繼承而來的實現是靜態的,不可能在運行時發生改變,沒有足夠的靈活性;只能在有限的環境中使用。(“白箱”複用)組合/聚合複用:耦合度相對較低,選擇性地調用成員對象的操作;可以在運行時動態進行。(“黑箱”複用)2)組合/聚合可以使系統更加靈活,類與類之間的耦合度降低,一個類的變化對其他類造成的影響相對較少,因此一般首選使用組合/聚合來實現複用;其次才考慮繼承,在使用繼承時,需要嚴格遵循里氏代換原則,有效使用繼承會有助於對問題的理解,降低複雜度,而濫用繼承反而會增加系統構建和維護的難度以及系統的複雜度,因此需要慎重使用繼承複用。3)此原則和里氏代換原則氏相輔相成的,兩者都是具體實現"開-閉"原則的規範。違反這一原則,就無法實現"開-閉"原則,首先我們要明白合成和聚合的概念:什麼是合成?合成(組合):表示一個整體與部分的關係,指一個依託整體而存在的關係(整體與部分不可以分開),例如:一個人對他的房子和傢俱,其中他的房子和傢俱是不能被共享的,因爲那些東西都是他自己的。並且人沒了,這個也關係就沒了。這個例子就好像,烏雞百鳳丸這個產品,它是有烏雞和上等藥材合成而來的一樣。也比如網絡遊戲中的武器裝備合成一樣,多種東西合併爲一種超強的東西一樣。雖然組合表示的是一個整體與部分的關係,但是組合關係中部分和整體具有統一的生存期。一旦整體對象不存在,部分對象也將不存在,部分對象與整體對象之間具有同生共死的關係。在組合關係中,成員類是整體類的一部分,而且整體類可以控制成員類的生命週期,即成員類的存在依賴於整體類。在UML中,組合關係用帶實心菱形的直線表示。源碼:
- public class Head
- {
- private Mouth mouth;
- public Head() {
- mouth = new Mouth();
- }
- }
- public class Mouth
- {
- }
什麼是聚合?聚合:聚合是比合成關係的一種更強的依賴關係,也表示整體與部分的關係(整體與部分可以分開),例如,一個奔馳S360汽車,對奔馳S360引擎,奔馳S360輪胎的關係..這些關係就是帶有聚合性質的。因爲奔馳S360引擎和奔馳S360輪胎他們只能被奔馳S360汽車所用,離開了奔馳S360汽車,它們就失去了存在的意義。在我們的設計中,這樣的關係不應該頻繁出現.這樣會增大設計的耦合度。在面向對象中的聚合:通常在定義一個整體類後,再去分析這個整體類的組成結構,從而找出一些成員類,該整體類和成員類之間就形成了聚合關係。在聚合關係中,成員類是整體類的一部分,即成員對象是整體對象的一部分,但是成員對象可以脫離整體對象獨立存在。在UML中,聚合關係用帶空心菱形的直線表示。比如汽車和汽車引擎:
- public class Car
- {
- private Engine engine;
- public Car(Engine engine) {
- this.engine = engine;
- }
- public void setEngine(Engine engine) {
- this.engine = engine;
- }
- }
- public class Engine
- {
- }
明白了合成和聚合關係,再來理解合成/聚合原則應該就清楚了。要避免在系統設計中出現,一個類的繼承層次超過3次。如果這樣的話,可以考慮重構你的代碼,或者重新設計結構. 當然最好的辦法就是考慮使用合成/聚合原則。
6.迪米特法則(Law of Demeter,LoD:系統中的類,儘量不要與其他類互相作用,減少類之間的耦合度
定義:又叫最少知識原則(Least Knowledge Principle或簡寫爲LKP)幾種形式定義:
(1) 不要和“陌生人”說話。英文定義爲:Don't talk to strangers.
Ÿ(2) 只與你的直接朋友通信。英文定義爲:Talk only to your immediatefriends.
(3) 每一個軟件單位對其他的單位都只有最少的知識,而且侷限於那些與本單位密切相關的軟件單位。
簡單地說,也就是,一個對象應當對其它對象有儘可能少的瞭解。一個類應該對自己需要耦合或調用的類知道得最少,你(被耦合或調用的類)的內部是如何複雜都和我沒關係,那是你的事情,我就知道你提供的public方法,我就調用這麼多,其他的一概不關心。
法則分析:
1)朋友類:在迪米特法則中,對於一個對象,其朋友包括以下幾類:Ÿ(1) 當前對象本身(this);Ÿ(2) 以參數形式傳入到當前對象方法中的對象;Ÿ(3) 當前對象的成員對象;Ÿ(4) 如果當前對象的成員對象是一個集合,那麼集合中的元素也都是朋友;Ÿ(5) 當前對象所創建的對象。任何一個對象,如果滿足上面的條件之一,就是當前對象的“朋友”,否則就是“陌生人”。2)狹義法則和廣義法則:在狹義的迪米特法則中,如果兩個類之間不必彼此直接通信,那麼這兩個類就不應當發生直接的相互作用,如果其中的一個類需要調用另一個類的某一個方法的話,可以通過第三者轉發這個調用。狹義的迪米特法則:可以降低類之間的耦合,但是會在系統中增加大量的小方法並散落在系統的各個角落,它可以使一個系統的局部設計簡化,因爲每一個局部都不會和遠距離的對象有直接的關聯,但是也會造成系統的不同模塊之間的通信效率降低,使得系統的不同模塊之間不容易協調。廣義的迪米特法則:指對對象之間的信息流量、流向以及信息的影響的控制,主要是對信息隱藏的控制。信息的隱藏可以使各個子系統之間脫耦,從而允許它們獨立地被開發、優化、使用和修改,同時可以促進軟件的複用,由於每一個模塊都不依賴於其他模塊而存在,因此每一個模塊都可以獨立地在其他的地方使用。一個系統的規模越大,信息的隱藏就越重要,而信息隱藏的重要性也就越明顯。3)迪米特法則的主要用途:在於控制信息的過載。•在類的劃分上,應當儘量創建鬆耦合的類,類之間的耦合度越低,就越有利於複用,一個處在鬆耦合中的類一旦被修改,不會對關聯的類造成太大波及;•在類的結構設計上,每一個類都應當儘量降低其成員變量和成員函數的訪問權限;•在類的設計上,只要有可能,一個類型應當設計成不變類;•在對其他類的引用上,一個對象對其他對象的引用應當降到最低。例子:外觀模式前面提到: 設計模式(九)外觀模式Facade(結構型)
迪米特法則與設計模式Facade模式、Mediator模式使民無知
系統中的類,儘量不要與其他類互相作用,減少類之間的耦合度,因爲在你的系統中,擴展的時候,你可能需要修改這些類,而類與類之間的關係,決定了修改的複雜度,相互作用越多,則修改難度就越大,反之,如果相互作用的越小,則修改起來的難度就越小..例如A類依賴B類,則B類依賴C類,當你在修改A類的時候,你要考慮B類是否會受到影響,而B類的影響是否又會影響到C類. 如果此時C類再依賴D類的話,呵呵,我想這樣的修改有的受了。
7.接口隔離法則(Interface Segregation Principle,ISL):客戶端不應該依賴那些它不需要的接口。(這個法則與迪米特法則是相通的)
定義:客戶端不應該依賴那些它不需要的接口。另一種定義方法:一旦一個接口太大,則需要將它分割成一些更細小的接口,使用該接口的客戶端僅需知道與之相關的方法即可。注意,在該定義中的接口指的是所定義的方法。例如外面調用某個類的public方法。這個方法對外就是接口。原則分析:1)接口隔離原則是指使用多個專門的接口,而不使用單一的總接口。每一個接口應該承擔一種相對獨立的角色,不多不少,不幹不該乾的事,該乾的事都要幹。•(1)一個接口就只代表一個角色,每個角色都有它特定的一個接口,此時這個原則可以叫做“角色隔離原則”。•(2)接口僅僅提供客戶端需要的行爲,即所需的方法,客戶端不需要的行爲則隱藏起來,應當爲客戶端提供儘可能小的單獨的接口,而不要提供大的總接口。2)使用接口隔離原則拆分接口時,首先必須滿足單一職責原則,將一組相關的操作定義在一個接口中,且在滿足高內聚的前提下,接口中的方法越少越好。3)可以在進行系統設計時採用定製服務的方式,即爲不同的客戶端提供寬窄不同的接口,只提供用戶需要的行爲,而隱藏用戶不需要的行爲。例子:下圖展示了一個擁有多個客戶類的系統,在系統中定義了一個巨大的接口(胖接口)AbstractService來服務所有的客戶類。可以使用接口隔離原則對其進行重構。重構後:
迪米特法則是目的,而接口隔離法則是對迪米特法則的規範. 爲了做到儘可能小的耦合性,我們需要使用接口來規範類,用接口來約束類.要達到迪米特法則的要求,最好就是實現接口隔離法則,實現接口隔離法則,你也就滿足了迪米特法則。
原文地址:hguisu 面向對象設計模式原則詳解 http://blog.csdn.net/hguisu/article/details/7571617