一、什麼是合成/聚合複用原則?
合成/聚合複用原則是在一個新的對象裡面使用一些已有的對象,使之成為新對象的一部分;新的對象通過向這些對象的委派達到複用已有功能的目的。
簡述為:要盡量使用合成/聚合,盡量不要使用繼承。
二、合成和聚合的差別;依賴和關聯
合成(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”關系。