設計模式簡介

九命丿相柳IP屬地: 上海
字數 8,782

title: 設計模式簡介
categories: 設計模式
tags: 設計模式
date: 2017-05-03 08:00:00
updated: 2017-05-03 08:00:00


設計模式的分類

總體來說設計模式分為三大類:

  • 創建型模式

    創建型模式共五種:

    工廠方法模式、抽象工廠模式、單例模式、建造者模式、原型模式。

  • 結構型模式

    結構型模式共七種:

    適配器模式、裝飾器模式、代理模式、外觀模式、橋接模式、組合模式、享元模式。

  • 行為型模式

    行為型模式共十一種:

    策略模式、模板方法模式、觀察者模式、迭代子模式、責任鏈模式、命令模式、備忘錄模式、狀態模式、訪問者模式、中介者模式、解釋器模式。

設計模式的六大原則

  • 總原則:開閉原則 (Open Close Principle)

    定義:一個軟件實體如類、模塊和函數應該對擴展開放,對修改關閉。

    問題由來:在軟件的生命周期內,因為變化、升級和維護等原因需要對軟件原有代碼進行修改時,可能會給舊代碼中引入錯誤,也可能會使我們不得不對整個功能進行重構,并且需要原有代碼經過重新測試。

    解決方案:當軟件需要變化時,盡量通過擴展軟件實體的行為來實現變化,而不是通過修改已有的代碼來實現變化。

    開閉原則是面向對象設計中最基礎的設計原則,它指導我們如何建立穩定靈活的系統。開閉原則可能是設計模式六項原則中定義最模糊的一個了,它只告訴我們對擴展開放,對修改關閉,可是到底如何才能做到對擴展開放,對修改關閉,并沒有明確的告訴我們。以前,如果有人告訴我“你進行設計的時候一定要遵守開閉原則”,我會覺的他什么都沒說,但貌似又什么都說了。因為開閉原則真的太虛了。

    在仔細思考以及仔細閱讀很多設計模式的文章后,終于對開閉原則有了一點認識。其實,我們遵循設計模式前面6大原則,以及使用23種設計模式的目的就是遵循開閉原則。也就是說,只要我們對前面5項原則遵守的好了,設計出的軟件自然是符合開閉原則的,這個開閉原則更像是前面五項原則遵守程度的“平均得分”,前面5項原則遵守的好,平均分自然就高,說明軟件設計開閉原則遵守的好;如果前面5項原則遵守的不好,則說明開閉原則遵守的不好。

    其實筆者認為,開閉原則無非就是想表達這樣一層意思:用抽象構建框架,用實現擴展細節。因為抽象靈活性好,適應性廣,只要抽象的合理,可以基本保持軟件架構的穩定。而軟件中易變的細節,我們用從抽象派生的實現類來進行擴展,當軟件需要發生變化時,我們只需要根據需求重新派生一個實現類來擴展就可以了。當然前提是我們的抽象要合理,要對需求的變更有前瞻性和預見性才行。

    說到這里,再回想一下前面說的6項原則,恰恰是告訴我們用抽象構建框架,用實現擴展細節的注意事項而已:單一職責原則告訴我們實現類要職責單一;里氏替換原則告訴我們不要破壞繼承體系;依賴倒置原則告訴我們要面向接口編程;接口隔離原則告訴我們在設計接口的時候要精簡單一;迪米特法則告訴我們要降低耦合。而開閉原則是總綱,他告訴我們要對擴展開放,對修改關閉。

  • 單一職責原則 (Single responsibility principle)

    定義:不要存在多于一個導致類變更的原因。通俗的說,即一個類只負責一項職責。

    問題由來:類T負責兩個不同的職責:職責P1,職責P2。當由于職責P1需求發生改變而需要修改類T時,有可能會導致原本運行正常的職責P2功能發生故障。

    解決方案:遵循單一職責原則。分別建立兩個類T1、T2,使T1完成職責P1功能,T2完成職責P2功能。這樣,當修改類T1時,不會使職責P2發生故障風險;同理,當修改T2時,也不會使職責P1發生故障風險。

    說到單一職責原則,很多人都會不屑一顧。因為它太簡單了。稍有經驗的程序員即使從來沒有讀過設計模式、從來沒有聽說過單一職責原則,在設計軟件時也會自覺的遵守這一重要原則,因為這是常識。在軟件編程中,誰也不希望因為修改了一個功能導致其他的功能發生故障。而避免出現這一問題的方法便是遵循單一職責原則。雖然單一職責原則如此簡單,并且被認為是常識,但是即便是經驗豐富的程序員寫出的程序,也會有違背這一原則的代碼存在。為什么會出現這種現象呢?因為有職責擴散。所謂職責擴散,就是因為某種原因,職責P被分化為粒度更細的職責P1和P2。

    比如:類T只負責一個職責P,這樣設計是符合單一職責原則的。后來由于某種原因,也許是需求變更了,也許是程序的設計者境界提高了,需要將職責P細分為粒度更細的職責P1,P2,這時如果要使程序遵循單一職責原則,需要將類T也分解為兩個類T1和T2,分別負責P1、P2兩個職責。但是在程序已經寫好的情況下,這樣做簡直太費時間了。所以,簡單的修改類T,用它來負責兩個職責是一個比較不錯的選擇,雖然這樣做有悖于單一職責原則。(這樣做的風險在于職責擴散的不確定性,因為我們不會想到這個職責P,在未來可能會擴散為P1,P2,P3,P4……Pn。所以記住,在職責擴散到我們無法控制的程度之前,立刻對代碼進行重構。)

    舉例說明,用一個類描述動物呼吸這個場景:

    class Animal{
        public void breathe(String animal){
            System.out.println(animal+"呼吸空氣");
        }
    }
    public class Client{
        public static void main(String[] args){
            Animal animal = new Animal();
            animal.breathe("牛");
            animal.breathe("羊");
            animal.breathe("豬");
        }
    }
    

    運行結果:

    牛呼吸空氣
    羊呼吸空氣
    豬呼吸空氣
    

    程序上線后,發現問題了,并不是所有的動物都呼吸空氣的,比如魚就是呼吸水的。修改時如果遵循單一職責原則,需要將Animal類細分為陸生動物類Terrestrial,水生動物Aquatic,代碼如下:

    class Terrestrial{
        public void breathe(String animal){
            System.out.println(animal+"呼吸空氣");
        }
    }
    class Aquatic{
        public void breathe(String animal){
            System.out.println(animal+"呼吸水");
        }
    }
    public class Client{
        public static void main(String[] args){
            Terrestrial terrestrial = new Terrestrial();
            terrestrial.breathe("牛");
            terrestrial.breathe("羊");
            terrestrial.breathe("豬");
            
            Aquatic aquatic = new Aquatic();
            aquatic.breathe("魚");
        }
    }
    

    運行結果:

    牛呼吸空氣
    羊呼吸空氣
    豬呼吸空氣
    魚呼吸水
    

    我們會發現如果這樣修改花銷是很大的,除了將原來的類分解之外,還需要修改客戶端。而直接修改類Animal來達成目的雖然違背了單一職責原則,但花銷卻小的多,代碼如下:

    class Animal{
        public void breathe(String animal){
            if("魚".equals(animal)){
                System.out.println(animal+"呼吸水");
            }else{
                System.out.println(animal+"呼吸空氣");
            }
        }
    }
    public class Client{
        public static void main(String[] args){
            Animal animal = new Animal();
            animal.breathe("牛");
            animal.breathe("羊");
            animal.breathe("豬");
            animal.breathe("魚");
        }
    }
    

    可以看到,這種修改方式要簡單的多。但是卻存在著隱患:有一天需要將魚分為呼吸淡水的魚和呼吸海水的魚,則又需要修改Animal類的breathe方法,而對原有代碼的修改會對調用“豬”“牛”“羊”等相關功能帶來風險,也許某一天你會發現程序運行的結果變為“牛呼吸水”了。這種修改方式直接在代碼級別上違背了單一職責原則,雖然修改起來最簡單,但隱患卻是最大的。還有一種修改方式:

    class Animal{
        public void breathe(String animal){
            System.out.println(animal+"呼吸空氣");
        }
        public void breathe2(String animal){
            System.out.println(animal+"呼吸水");
        }
    }
    public class Client{
        public static void main(String[] args){
            Animal animal = new Animal();
            animal.breathe("牛");
            animal.breathe("羊");
            animal.breathe("豬");
            animal.breathe2("魚");
        }
    }
    

    可以看到,這種修改方式沒有改動原來的方法,而是在類中新加了一個方法,這樣雖然也違背了單一職責原則,但在方法級別上卻是符合單一職責原則的,因為它并沒有動原來方法的代碼。這三種方式各有優缺點,那么在實際編程中,采用哪一中呢?其實這真的比較難說,需要根據實際情況來確定。我的原則是:只有邏輯足夠簡單,才可以在代碼級別上違反單一職責原則;只有類中方法數量足夠少,才可以在方法級別上違反單一職責原則;

    例如本文所舉的這個例子,它太簡單了,它只有一個方法,所以,無論是在代碼級別上違反單一職責原則,還是在方法級別上違反,都不會造成太大的影響。實際應用中的類都要復雜的多,一旦發生職責擴散而需要修改類時,除非這個類本身非常簡單,否則還是遵循單一職責原則的好。

    遵循單一職責原的優點有:

    1. 可以降低類的復雜度,一個類只負責一項職責,其邏輯肯定要比負責多項職責簡單的多;
    2. 提高類的可讀性,提高系統的可維護性;
    3. 變更引起的風險降低,變更是必然的,如果單一職責原則遵守的好,當修改一個功能時,可以顯著降低對其他功能的影響;
    4. 需要說明的一點是單一職責原則不只是面向對象編程思想所特有的,只要是模塊化的程序設計,都適用單一職責原則。
  • 里氏替換原則 (Liskov Substitution Principle)

    定義:如果對每一個類型為T1的對象o1,都有類型為T2的對象o2,使得以T1定義的所有程序P在所有的對象o1都代換成o2時,程序 P 的行為沒有發生變化,那么類型 T2 是類型 T1 的子類型。所有引用基類的地方必須能透明地使用其子類的對象。

    問題由來:有一功能P1,由類A完成。現需要將功能P1進行擴展,擴展后的功能為P,其中P由原有功能P1與新功能P2組成。新功能P由類A的子類B來完成,則子類B在完成新功能P2的同時,有可能會導致原有功能P1發生故障。

    解決方案:當使用繼承時,遵循里氏替換原則。類B繼承類A時,除添加新的方法完成新增功能P2外,盡量不要重寫父類A的方法,也盡量不要重載父類A的方法。

    繼承包含這樣一層含義:父類中凡是已經實現好的方法(相對于抽象方法而言),實際上是在設定一系列的規范和契約,雖然它不強制要求所有的子類必須遵從這些契約,但是如果子類對這些非抽象方法任意修改,就會對整個繼承體系造成破壞。而里氏替換原則就是表達了這一層含義。

    繼承作為面向對象三大特性之一,在給程序設計帶來巨大便利的同時,也帶來了弊端。比如使用繼承會給程序帶來侵入性,程序的可移植性降低,增加了對象間的耦合性,如果一個類被其他的類所繼承,則當這個類需要修改時,必須考慮到所有的子類,并且父類修改后,所有涉及到子類的功能都有可能會產生故障。

    舉例說明繼承的風險,我們需要完成一個兩數相減的功能,由類A來負責。

    class A{
        public int func1(int a, int b){
            return a-b;
        }
    }
    public class Client{
        public static void main(String[] args){
            A a = new A();
            System.out.println("100-50="+a.func1(100, 50));
            System.out.println("100-80="+a.func1(100, 80));
        }
    }
    

    運行結果:

    100-50=50
    100-80=20
    

    后來,我們需要增加一個新的功能:完成兩數相加,然后再與100求和,由類B來負責。即類B需要完成兩個功能:
    兩數相減。

    兩數相加,然后再加100。

    由于類A已經實現了第一個功能,所以類B繼承類A后,只需要再完成第二個功能就可以了,代碼如下:

    class B extends A {
        public int func1(int a, int b){
            return a+b;
        }
        
        public int func2(int a, int b){
            return func1(a,b)+100;
        }
    }
    public class Client{
        public static void main(String[] args){
            B b = new B();
            System.out.println("100-50="+b.func1(100, 50));
            System.out.println("100-80="+b.func1(100, 80));
            System.out.println("100+20+100="+b.func2(100, 20));
        }
    }
    

    類B完成后,運行結果:

    100-50=150
    100-80=180
    100+20+100=220
    

    我們發現原本運行正常的相減功能發生了錯誤。原因就是類B在給方法起名時無意中重寫了父類的方法,造成所有運行相減功能的代碼全部調用了類B重寫后的方法,造成原本運行正常的功能出現了錯誤。在本例中,引用基類A完成的功能,換成子類B之后,發生了異常。在實際編程中,我們常常會通過重寫父類的方法來完成新的功能,這樣寫起來雖然簡單,但是整個繼承體系的可復用性會比較差,特別是運用多態比較頻繁時,程序運行出錯的幾率非常大。如果非要重寫父類的方法,比較通用的做法是:原來的父類和子類都繼承一個更通俗的基類,原有的繼承關系去掉,采用依賴、聚合,組合等關系代替。

    里氏替換原則通俗的來講就是:子類可以擴展父類的功能,但不能改變父類原有的功能。它包含以下4層含義:

      1. 子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法。
      2. 子類中可以增加自己特有的方法。
      3. 當子類的方法重載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬松。
      4. 當子類的方法實現父類的抽象方法時,方法的后置條件(即方法的返回值)要比父類更嚴格。
    

    看上去很不可思議,因為我們會發現在自己編程中常常會違反里氏替換原則,程序照樣跑的好好的。所以大家都會產生這樣的疑問,假如我非要不遵循里氏替換原則會有什么后果?

    后果就是:你寫的代碼出問題的幾率將會大大增加。

  • 依賴倒轉原則 (Dependence Inversion Principle)

    定義:高層模塊不應該依賴低層模塊,二者都應該依賴其抽象;抽象不應該依賴細節;細節應該依賴抽象。

    問題由來:類A直接依賴類B,假如要將類A改為依賴類C,則必須通過修改類A的代碼來達成。這種場景下,類A一般是高層模塊,負責復雜的業務邏輯;類B和類C是低層模塊,負責基本的原子操作;假如修改類A,會給程序帶來不必要的風險。

    解決方案:將類A修改為依賴接口I,類B和類C各自實現接口I,類A通過接口I間接與類B或者類C發生聯系,則會大大降低修改類A的幾率。

    依賴倒置原則基于這樣一個事實:相對于細節的多變性,抽象的東西要穩定的多。以抽象為基礎搭建起來的架構比以細節為基礎搭建起來的架構要穩定的多。在java中,抽象指的是接口或者抽象類,細節就是具體的實現類,使用接口或者抽象類的目的是制定好規范和契約,而不去涉及任何具體的操作,把展現細節的任務交給他們的實現類去完成。

    依賴倒置原則的核心思想是面向接口編程,我們依舊用一個例子來說明面向接口編程比相對于面向實現編程好在什么地方。場景是這樣的,母親給孩子講故事,只要給她一本書,她就可以照著書給孩子講故事了。代碼如下:

    class Book{
        public String getContent(){
            return "很久很久以前有一個阿拉伯的故事……";
        }
    }
    class Mother{
        public void narrate(Book book){
            System.out.println("媽媽開始講故事");
            System.out.println(book.getContent());
        }
    }
    public class Client{
        public static void main(String[] args){
            Mother mother = new Mother();
            mother.narrate(new Book());
        }
    }
    

    運行結果:

    媽媽開始講故事
    很久很久以前有一個阿拉伯的故事……
    

    運行良好,假如有一天,需求變成這樣:不是給書而是給一份報紙,讓這位母親講一下報紙上的故事,報紙的代碼如下:

    class Newspaper{
        public String getContent(){
            return "林書豪38+7領導尼克斯擊敗湖人……";
        }
    }
    

    這位母親卻辦不到,因為她居然不會讀報紙上的故事,這太荒唐了,只是將書換成報紙,居然必須要修改Mother才能讀。假如以后需求換成雜志呢?換成網頁呢?還要不斷地修改Mother,這顯然不是好的設計。原因就是Mother與Book之間的耦合性太高了,必須降低他們之間的耦合度才行。

    我們引入一個抽象的接口IReader。讀物,只要是帶字的都屬于讀物:

    interface IReader{
        public String getContent();
    }
    

    Mother類與接口IReader發生依賴關系,而Book和Newspaper都屬于讀物的范疇,他們各自都去實現IReader接口,這樣就符合依賴倒置原則了,代碼修改為:

    class Newspaper implements IReader {
        public String getContent(){
            return "林書豪17+9助尼克斯擊敗老鷹……";
        }
    }
    class Book implements IReader{
        public String getContent(){
            return "很久很久以前有一個阿拉伯的故事……";
        }
    }
    class Mother{
        public void narrate(IReader reader){
            System.out.println("媽媽開始講故事");
            System.out.println(reader.getContent());
        }
    }
    public class Client{
        public static void main(String[] args){
            Mother mother = new Mother();
            mother.narrate(new Book());
            mother.narrate(new Newspaper());
        }
    }
    

    運行結果:

    媽媽開始講故事
    很久很久以前有一個阿拉伯的故事……
    媽媽開始講故事
    林書豪17+9助尼克斯擊敗老鷹……
    

    這樣修改后,無論以后怎樣擴展Client類,都不需要再修改Mother類了。這只是一個簡單的例子,實際情況中,代表高層模塊的Mother類將負責完成主要的業務邏輯,一旦需要對它進行修改,引入錯誤的風險極大。所以遵循依賴倒置原則可以降低類之間的耦合性,提高系統的穩定性,降低修改程序造成的風險。

    采用依賴倒置原則給多人并行開發帶來了極大的便利,比如上例中,原本Mother類與Book類直接耦合時,Mother類必須等Book類編碼完成后才可以進行編碼,因為Mother類依賴于Book類。修改后的程序則可以同時開工,互不影響,因為Mother與Book類一點關系也沒有。參與協作開發的人越多、項目越龐大,采用依賴導致原則的意義就越重大。現在很流行的TDD開發模式就是依賴倒置原則最成功的應用。

    傳遞依賴關系有三種方式,以上的例子中使用的方法是接口傳遞,另外還有兩種傳遞方式:構造方法傳遞和setter方法傳遞,相信用過Spring框架的,對依賴的傳遞方式一定不會陌生。

    在實際編程中,我們一般需要做到如下3點:

      1. 低層模塊盡量都要有抽象類或接口,或者兩者都有。
      2. 變量的聲明類型盡量是抽象類或接口。
      3. 使用繼承時遵循里氏替換原則。
    

    依賴倒置原則的核心就是要我們面向接口編程,理解了面向接口編程,也就理解了依賴倒置。

  • 接口隔離原則 (Interface Segregation Principle)

    定義:客戶端不應該依賴它不需要的接口;一個類對另一個類的依賴應該建立在最小的接口上。

    問題由來:類A通過接口I依賴類B,類C通過接口I依賴類D,如果接口I對于類A和類B來說不是最小接口,則類B和類D必須去實現他們不需要的方法。

    解決方案:將臃腫的接口I拆分為獨立的幾個接口,類A和類C分別與他們需要的接口建立依賴關系。也就是采用接口隔離原則。
    舉例來說明接口隔離原則:

    圖1 未遵循接口隔離原則的設計

    這個圖的意思是:類A依賴接口I中的方法1、方法2、方法3,類B是對類A依賴的實現。類C依賴接口I中的方法1、方法4、方法5,類D是對類C依賴的實現。對于類B和類D來說,雖然他們都存在著用不到的方法(也就是圖中紅色字體標記的方法),但由于實現了接口I,所以也必須要實現這些用不到的方法。對類圖不熟悉的可以參照程序代碼來理解,代碼如下:

    可以看到,如果接口過于臃腫,只要接口中出現的方法,不管對依賴于它的類有沒有用處,實現類中都必須去實現這些方法,這顯然不是好的設計。如果將這個設計修改為符合接口隔離原則,就必須對接口I進行拆分。在這里我們將原有的接口I拆分為三個接口,拆分后的設計如圖2所示:

    圖2 遵循接口隔離原則的設計

    接口隔離原則的含義是:建立單一接口,不要建立龐大臃腫的接口,盡量細化接口,接口中的方法盡量少。也就是說,我們要為各個類建立專用的接口,而不要試圖去建立一個很龐大的接口供所有依賴它的類去調用。本文例子中,將一個龐大的接口變更為3個專用的接口所采用的就是接口隔離原則。在程序設計中,依賴幾個專用的接口要比依賴一個綜合的接口更靈活。接口是設計時對外部設定的“契約”,通過分散定義多個接口,可以預防外來變更的擴散,提高系統的靈活性和可維護性。

    說到這里,很多人會覺的接口隔離原則跟之前的單一職責原則很相似,其實不然。其一,單一職責原則原注重的是職責;而接口隔離原則注重對接口依賴的隔離。其二,單一職責原則主要是約束類,其次才是接口和方法,它針對的是程序中的實現和細節;而接口隔離原則主要約束接口接口,主要針對抽象,針對程序整體框架的構建。

    采用接口隔離原則對接口進行約束時,要注意以下幾點:

      1. 接口盡量小,但是要有限度。對接口進行細化可以提高程序設計靈活性是不掙的事實,但是如果過小,則會造成接口數量過多,使設計復雜化。所以一定要適度。
      2. 為依賴接口的類定制服務,只暴露給調用的類它需要的方法,它不需要的方法則隱藏起來。只有專注地為一個模塊提供定制服務,才能建立最小的依賴關系。
      3. 提高內聚,減少對外交互。使接口用最少的方法去完成最多的事情。
    

    運用接口隔離原則,一定要適度,接口設計的過大或過小都不好。設計接口的時候,只有多花些時間去思考和籌劃,才能準確地實踐這一原則。

  • 迪米特法則 (Demeter Principle)

    定義:一個對象應該對其他對象保持最少的了解(最少知道原則)。

    問題由來:類與類之間的關系越密切,耦合度越大,當一個類發生改變時,對另一個類的影響也越大。

    解決方案:盡量降低類與類之間的耦合。

    自從我們接觸編程開始,就知道了軟件編程的總的原則:低耦合,高內聚。無論是面向過程編程還是面向對象編程,只有使各個模塊之間的耦合盡量的低,才能提高代碼的復用率。低耦合的優點不言而喻,但是怎么樣編程才能做到低耦合呢?那正是迪米特法則要去完成的。

    迪米特法則又叫最少知道原則,最早是在1987年由美國Northeastern University的Ian Holland提出。通俗的來講,就是一個類對自己依賴的類知道的越少越好。也就是說,對于被依賴的類來說,無論邏輯多么復雜,都盡量地的將邏輯封裝在類的內部,對外除了提供的public方法,不對外泄漏任何信息。迪米特法則還有一個更簡單的定義:只與直接的朋友通信。首先來解釋一下什么是直接的朋友:每個對象都會與其他對象有耦合關系,只要兩個對象之間有耦合關系,我們就說這兩個對象之間是朋友關系。耦合的方式很多,依賴、關聯、組合、聚合等。其中,我們稱出現成員變量、方法參數、方法返回值中的類為直接的朋友,而出現在局部變量中的類則不是直接的朋友。也就是說,陌生的類最好不要作為局部變量的形式出現在類的內部。

    舉一個例子:有一個集團公司,下屬單位有分公司和直屬部門,現在要求打印出所有下屬單位的員工ID。先來看一下違反迪米特法則的設計。

    //總公司員工
    class Employee{
        private String id;
        public void setId(String id){
            this.id = id;
        }
        public String getId(){
            return id;
        }
    }
    //分公司員工
    class SubEmployee{
        private String id;
        public void setId(String id){
            this.id = id;
        }
        public String getId(){
            return id;
        }
    }
    class SubCompanyManager{
        public List<SubEmployee> getAllEmployee(){
            List<SubEmployee> list = new ArrayList<SubEmployee>();
            for(int i=0; i<100; i++){
                SubEmployee emp = new SubEmployee();
                //為分公司人員按順序分配一個ID
                emp.setId("分公司"+i);
                list.add(emp);
            }
            return list;
        }
    }
    class CompanyManager{
        public List<Employee> getAllEmployee(){
            List<Employee> list = new ArrayList<Employee>();
            for(int i=0; i<30; i++){
                Employee emp = new Employee();
                //為總公司人員按順序分配一個ID
                emp.setId("總公司"+i);
                list.add(emp);
            }
            return list;
        }
        
        public void printAllEmployee(SubCompanyManager sub){
            List<SubEmployee> list1 = sub.getAllEmployee();
            for(SubEmployee e:list1){
                System.out.println(e.getId());
            }
            List<Employee> list2 = this.getAllEmployee();
            for(Employee e:list2){
                System.out.println(e.getId());
            }
        }
    }
    public class Client{
        public static void main(String[] args){
            CompanyManager e = new CompanyManager();
            e.printAllEmployee(new SubCompanyManager());
        }
    }
    

    現在這個設計的主要問題出在CompanyManager中,根據迪米特法則,只與直接的朋友發生通信,而SubEmployee類并不是CompanyManager類的直接朋友(以局部變量出現的耦合不屬于直接朋友),從邏輯上講總公司只與他的分公司耦合就行了,與分公司的員工并沒有任何聯系,這樣設計顯然是增加了不必要的耦合。按照迪米特法則,應該避免類中出現這樣非直接朋友關系的耦合。

    修改后的代碼如下:

    class SubCompanyManager{
        public List<SubEmployee> getAllEmployee(){
            List<SubEmployee> list = new ArrayList<SubEmployee>();
            for(int i=0; i<100; i++){
                SubEmployee emp = new SubEmployee();
                //為分公司人員按順序分配一個ID
                emp.setId("分公司"+i);
                list.add(emp);
            }
            return list;
        }
        public void printEmployee(){
            List<SubEmployee> list = this.getAllEmployee();
            for(SubEmployee e:list){
                System.out.println(e.getId());
            }
        }
    }
    class CompanyManager{
        public List<Employee> getAllEmployee(){
            List<Employee> list = new ArrayList<Employee>();
            for(int i=0; i<30; i++){
                Employee emp = new Employee();
                //為總公司人員按順序分配一個ID
                emp.setId("總公司"+i);
                list.add(emp);
            }
            return list;
        }
        
        public void printAllEmployee(SubCompanyManager sub){
            sub.printEmployee();
            List<Employee> list2 = this.getAllEmployee();
            for(Employee e:list2){
                System.out.println(e.getId());
            }
        }
    }
    

    修改后,為分公司增加了打印人員ID的方法,總公司直接調用來打印,從而避免了與分公司的員工發生耦合。

    迪米特法則的初衷是降低類之間的耦合,由于每個類都減少了不必要的依賴,因此的確可以降低耦合關系。但是凡事都有度,雖然可以避免與非直接的類通信,但是要通信,必然會通過一個“中介”來發生聯系,例如本例中,總公司就是通過分公司這個“中介”來與分公司的員工發生聯系的。過分的使用迪米特原則,會產生大量這樣的中介和傳遞類,導致系統復雜度變大。所以在采用迪米特法則時要反復權衡,既做到結構清晰,又要高內聚低耦合。

  • 合成復用原則 (Composite Reuse Principle)

    定義:盡量使用對象組合,而不是繼承來達到復用的目的。

    問題由來:在面向對象設計中,可以通過兩種方法在不同的環境中復用已有的設計和實現,即通過組合/聚合關系或通過繼承,但首先應該考慮使用組合/聚合,組合/聚合可以使系統更加靈活,降低類與類之間的耦合度,一個類的變化對其他類造成的影響相對較少;其次才考慮繼承,在使用繼承時,需要嚴格遵循里氏代換原則,有效使用繼承會有助于對問題的理解,降低復雜度,而濫用繼承反而會增加系統構建和維護的難度以及系統的復雜度,因此需要慎重使用繼承復用。通過繼承來進行復用的主要問題在于繼承復用會破壞系統的封裝性,因為繼承會將基類的實現細節暴露給子類,由于基類的內部細節通常對子類來說是可見的,所以這種復用又稱“白箱”復用,如果基類發生改變,那么子類的實現也不得不發生改變;從基類繼承而來的實現是靜態的,不可能在運行時發生改變,沒有足夠的靈活性;而且繼承只能在有限的環境中使用(如類沒有聲明為不能被繼承)。

    解決方案:在實現復用時應該多用關聯,少用繼承。

最后編輯于
©著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
0人點贊
九命丿相柳滿城花醉三千客,一劍霜寒十九州。
總資產1共写了2.2W字获得8个赞共4个粉丝
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容

  • 目錄: 設計模式六大原則(1):單一職責原則 設計模式六大原則(2):里氏替換原則 設計模式六大原則(3):依賴倒...
    加油小杜閱讀 735評論 0 1
  • 設計模式六大原則 設計模式六大原則(1):單一職責原則 定義:不要存在多于一個導致類變更的原因。通俗的說,即一個類...
    viva158閱讀 779評論 0 1
  • 設計模式六大原則(1):單一職責原則 定義:不要存在多于一個導致類變更的原因。通俗的說,即一個類只負責一項職責。 ...
    Jabir_Zhang閱讀 650評論 0 3
  • 生活之所以痛苦,是因為太過執著于自認為正確的錯誤。 生活之所以痛苦,是因為不休比較期待與現實間的差距。 生活之所以...
    顧西白閱讀 269評論 0 0
  • 練個喜歡的人都沒有是不是人生太失敗了 從小到大一直幻想有童話中的白馬王子,但是長的總是好的,現實往往不會那么順利的...
    小太陽蒙蒙噠閱讀 191評論 0 0