1. 程式人生 > >Java設計模式——合成/聚合複用原則

Java設計模式——合成/聚合複用原則

一、什麼是合成/聚合複用原則? 合成/聚合複用原則是在一個新的物件裡面使用一些已有的物件,使之成為新物件的一部分;新的物件通過向這些物件的委派達到複用已有功能的目的。 簡述為:要儘量使用合成/聚合,儘量不要使用繼承。 二、合成和聚合的區別;依賴和關聯 合成(Composition)和聚合(Aggregation)都是關聯(Association)的特殊種類。用C語言來講,合成是值的聚合(Aggregation by Value),聚合是則是引用的聚合(Aggregation by Reference)。 (1)聚合用來表示“擁有”關係或者整體與部分的關係。代表部分的物件有可能會被多個代表整體的物件所共享,而且不一定會隨著某個代表整體的物件被銷燬或破壞而被銷燬或破壞,部分的生命週期可以超越整體。例如,班級和學生,當班級刪除後,學生還能存在,學生可以被培訓機構引用。 聚合關係UML類圖

class Student { } class Classes{          privateStudent student;          publicClasses(Student student){                    this.student=student;         } }   (2)合成用來表示一種強得多的“擁有”關係。在一個合成關係裡,部分和整體的生命週期是一樣的。一個合成的新物件完全擁有對其組成部分的支配權,包括它們的建立和湮滅等。使用程式語言的術語來說,合成而成的新物件對組成部分的記憶體分配、記憶體釋放有絕對的責任。 一個合成關係中的成分物件是不能與另一個合成關係共享的。一個成分物件在同一個時間內只能屬於一個合成關係。如果一個合成關係湮滅了,那麼所有的成分物件要麼自己湮滅所有的成分物件(這種情況較為普遍)要麼就得將這一責任交給別人(較為罕見)。 例如,一個人由頭、四肢和各種器官組成,人與這些具有相同的生命週期,人死了,這些器官也就掛了。房子和房間的關係,當房子沒了,房間也不可能獨立存在。 合成關係UML類圖

class Room{          public Room createRoom(){                    System.out.println(“建立房間”);                    returnnew Room();           }  } class House{          private Room room;          public House(){                room=new Room();           }          public void createHouse(){                 room.createRoom();          }   } (3)依賴和關聯 依賴(Dependency) 依賴是類與類之間的連線,表示一個類依賴於另外一個類的定義。依賴關係僅僅描述了類與類之間的一種使用與被使用的關係,在Java中體現為區域性變數、方法的引數或者是對靜態方法的呼叫。 依賴關係UML類圖

static class Boat{        public static void row(){            System.out.println("開動");        } } class Person{         public void crossRiver(Boatboat){             boat.row();         }                   public void fishing(){             Boat boat =new Boat() ;             boat.row();         }        public void patrol(){             Boat.row() ;        } } 關聯(Association) 關聯是類與類之間的連結。關聯關係使一個類知道另外一個類的屬性和方法。關聯可以是雙向的,也可以是單向的。體現在Java中,關聯關係是通過成員變數來實現的。 一般關聯關係UML類圖

class Computer{     public void develop(){        System.out.println("Develop ");     } } class Person{        private Computer computer ;                  public Person(Computer computer){            this.computer = computer ;        }                  public void work(){            computer.develop() ;            System.out.println("work");        }  } 三、為什麼使用合成/聚合複用,而不使用繼承複用? 在面向物件的設計裡,有兩種基本的方法可以在不同的環境中複用已有的設計和實現,即通過合成/聚合複用和通過繼承複用。兩者的特點和區別,優點和缺點如下。 1、合成/聚合複用 由於合成或聚合可以將已有物件納入到新物件中,使之成為新物件的一部分,因此新物件可以呼叫已有物件的功能。這樣做的好處有 (1)      新物件存取成分物件的唯一方法是通過成分物件的介面。 (2)      這種複用是黑箱複用,因為成分物件的內部細節是新物件看不見的。 (3)      這種複用支援包裝。 (4)      這種複用所需的依賴較少。 (5)      每一個新的類可以將焦點集中到一個任務上。 (6)      這種複用可以再執行時間內動態進行,新物件可以動態地引用與成分物件型別相同的物件。 一般而言,如果一個角色得到了更多的責任,那麼可以使用合成/聚合關係將新的責任委派到合適的物件。當然,這種複用也有缺點。最主要的缺點就是通過這種複用建造的系統會有較多的物件需要管理。 2、繼承複用 繼承複用通過擴充套件一個已有物件的實現來得到新的功能,基類明顯的捕獲共同的屬性和方法,而子類通過增加新的屬性和方法來擴充套件超類的實現。繼承是型別的複用。 繼承複用的優點。 (1)      新的實現較為容易,因為超類的大部分功能可以通過繼承關係自動進入子類。 (2)      修改或擴充套件繼承而來的實現較為容易。 繼承複用的缺點。 (1)      繼承複用破壞包裝,因為繼承將超類的實現細節暴露給了子類。因為超類的內部細節常常對子類是透明的,因此這種複用是透明的複用,又叫“白箱”複用。 (2)      如果超類的實現改變了,那麼子類的實現也不得不發生改變。因此,當一個基類發生了改變時,這種改變會傳導到一級又一級的子類,使得設計師不得不相應的改變這些子類,以適應超類的變化。 (3)      從超類繼承而來的實現是靜態的,不可能在執行時間內發生變化,因此沒有足夠的靈活性。 由於繼承複用有以上的缺點,所有儘量使用合成/聚合而不是繼承來達到對實現的複用,是非常重要的設計原則。 四、從程式碼重構的角度理解 一般來說,對於違反里氏代換原則的設計進行重構時,可以採取兩種方法:一是加入一個抽象超類;二是將繼承關係改寫為合成/聚合關係。 要正確的使用繼承關係,必須透徹的理解里氏代換原則和Coad條件。 區分“Has-A”和“Is -A” “Is-A”是嚴格的分類學意義上的定義,意思是一個類是另以個類的“一種”。而“Has-A”表示某一個角色具有某一項責任。 導致錯誤的使用繼承而不是合成/聚合的一個常見原因是錯誤的把“Has-A”當做“Is-A”。“Is-A”代表一個類是另一個類的一種;“Has-A”代表一個類是另一個類的一個角色,而不是另一個類的一個特殊種類。這是Coad條件的第一條。 下面類圖中描述的例子。“人”被繼承到“學生”、“經理”和“僱員”等子類。而實際上,學生”、“經理”和“僱員”分別描述一種角色,而“人”可以同時有幾種不同的角色。比如,一個人既然是“經理”,就必然是“僱員”;而“人”可能同時還參加MBA課程,從而也是一個“學生”。使用繼承來實現角色,則只能使每一個“人”具有Is-A角色,而且繼承是靜態的,這會使得一個“人”在成為“僱員”身份後,就永遠為“僱員”,不能成為“學生”和“經理”,而這顯然是不合理的。

這一錯誤的設計源自於把“角色”的等級結構和“人”的等級結構混淆起來,把“Has-A”角色誤解為“Is -A”角色。因此要糾正這種錯誤,關鍵是區分“人”與“角色”的區別。下圖所示的的設計就正確的做到了這一點。

從上圖可以看出,每一個“人”都可以有一個以上的“角色”,所有一個“人”可以同時是“僱員”,又是“經理”,甚至同時又是“學生”。而且由於“人”與“角色”的耦合是通過合成的,因此,角色可以有動態的變化。一個“人”可以開始是“僱員”,然後晉升為“經理”,然後又由於他參加了MBA課程,又稱為了“學生“。 當一個類是另一個類的角色時,不應當使用繼承描述這種關係。 與里氏代換原則聯合使用 里氏代換原則是繼承複用的基石。如果在任何可以使用B型別的地方都可以使用S型別,那麼S型別才可以稱為B型別的子型別(SubType),而B型別才能稱為S型別的基型別(BaseType)。 換言之,只有當每一個S在任何情況下都是一種B的時候,才可以將S設計成B的子類。如果兩個類的關係是“Has-A”關係而不是“Is -A”,這兩個類一定違反里氏代換原則。 只有兩個類滿足里氏代換原則,才有可能是“Is -A”關係。