合成是什麼?聚合又是什麼?他們與繼承差在哪???
盡量使用組合(contains-a)/聚合(has-a)方式來代替繼承(is-a)來達到重複使用的目的
在軟體開發中,一定會遇到套件重複使用的問題。CARP主要目的在於當要重複使用套件時,應該先考慮使用組合/聚合的方式,其次才是繼承。而如果要使用繼承的話,則須符合里氏替換原則(LSP)。
在比較合成/聚合與繼承之間的差異性時,首先要先了解合成是什麼?聚合又是什麼?
我們可以把這兩個行為理解成整體與部分對象的"擁有"關係。
在聚合中,部分對象及整體對象兩者的生命週期並沒有關聯,部分對象甚至可能超過整體對象。例如部分對象是學生,而整體對象是班級。一個班級畢業班級就不存在了,但學生依舊繼續存在於另一個整體對象,如大學或是某間公司。學生(部分對象)的生命週期並沒有因為班級(整體對象)結束而跟著結束。
class Clazz {
public void request(Student student){
System.out.println("class點名!!");
student.response();
}
}
class Student {
public void response(){
System.out.println("student喊在!!");
}
}
public class MyClass {
public static void main(String args[]) {
Clazz c = new Clazz();
Student s = new Student();
c.request(s);
}
}
output
class點名!!
student喊在!!
相較於聚合,合成的擁有關係就相對強烈一點。部分對象以及整體對象的生命週期是一致的。如人與他的心臟。如果他人死了,那心臟也跟著停止跳動。相對於聚合,合成擁有較強的關聯性。
class People {
Heart h = new Heart();
public void request(){
System.out.println("people走路!!");
h.response();
}
}
class Heart {
public void response(){
System.out.println("heart跳一下!!");
}
}
public class MyClass {
public static void main(String args[]) {
People c = new People();
c.request();
}
}
output
people走路!!
heart跳一下!!
經由上面兩個例子可以發現,Clazz及Student的關聯性低,就算Clazz結束了也跟Student無關。而相較於前一個例子,People與Heart的關係就比較緊密,若People生命週期結束了Heart也會跟著結束。
比較完合成及聚合的差異,現在來看看合成/聚合跟繼承又差在哪呢?
剛剛簡述了一下合成/聚合以及繼承的差別,可能還有些模糊,那我們來舉個例子。車子有分電動車以及汽油車,各有白色及黑色兩款,如下:
abstract class Car {
abstract void run();
}
class ElectricCar extends Car {
@Override
void run() {
System.out.println("電動車");
}
}
class PetrolCar extends Car {
@Override
void run() {
System.out.println("汽油車");
}
}
class BlackElectricCar extends ElectricCar {
public void appearance() {
System.out.print("黑色");
super.run();
}
}
class BlackPetrolCar extends PetrolCar {
public void appearance() {
System.out.print("黑色");
super.run();
}
}
class WhiteElectricCar extends ElectricCar {
public void appearance() {
System.out.print("白色");
super.run();
}
}
class WhitePetrolCar extends PetrolCar {
public void appearance() {
System.out.print("白色");
super.run();
}
}
public class MyCar {
public static void main(String args[]) {
WhiteElectricCar whiteElectricCar = new WhiteElectricCar();
whiteElectricCar.appearance();
}
}
output
白色電動車
這是用繼承的方式去撰寫一個汽車的系統,我們可以用剛剛比較的三點來看看:
封裝性:
封裝性不用講,繼承的封裝性對子類別來說一定是透明的。
耦合性:
電動/汽油車繼承汽車,不同種顏色的車載分別繼承電動/汽油車,耦合性相當的高
靈活性:
每當我想要某一個車種,我就必須直接建立該顏色車種的物件,相當的不方便。
現在來使用CARP來解決上述的問題。
建立一個interface Color,然後將這個interface合成到Car的抽象類別內。接著再由不同的顏色實作Color,如下:
interface Color { // 抽出來建立介面
void kind();
}
abstract class Car {
Color color; // 做合成
abstract void run();
public Color getColor() {
return color;
}
public void setColor(Color color) {
this.color = color;
}
}
class ElectricCar extends Car {
@Override
void run() {
System.out.println("電動車");
}
}
class PetrolCar extends Car {
@Override
void run() {
System.out.println("汽油車");
}
}
class White implements Color{
@Override
public void kind() {
System.out.println("白色");
}
}
class Black implements Color{
@Override
public void kind() {
System.out.println("黑色");
}
}
public class MyCar {
public static void main(String args[]) {
ElectricCar electricCar = new ElectricCar();
White color = new White();
electricCar.setColor(color);
electricCar.getColor().kind();
electricCar.run();
}
}
output
白色
電動車
把Color拉出來建立一個介面。再經由不同的顏色去實作,最後類別Car再由合成的方式將Color引入,這是用CARP的方式做更改後的結果。現在依然用剛剛比較的三點來看看:
封裝性:
實作Color的White和Black並沒有透露出他們實作的細節,外部只有使用他們的方法而已,保持完整的封裝性。
耦合性:
Color和Car沒有了相依,降低了類別間的耦合。
靈活性:
Color和Car沒有了相依,只需要選好車款,再把指定的顏色帶入,提升了靈活性。
經過班級與學生、人與器官的例子可以知道,耦合性聚合 < 合成。從車子的例子了解了耦合性合成 < 繼承。
優點
1. 保持封裝性
2. 類別間的耦合度低
3. 程式靈活度高
缺點
1. 建造的系統會有較多的類別需要管理
盡量使用組合(contains-a)/聚合(has-a)方式來代替繼承(is-a)來達到重複使用的目的
合成:部分對象以及整體對象的生命週期是一致的。
聚合:部分對象的生命週期與整體對象沒有關聯,甚至可能更長。
範例1:CARP-Aggregate
範例2:CARP-Composite
範例3:CARP-inherit
範例4:CARP
程序员必会的设计模式七大原则之——合成复用原则
合成复用原则——面向对象设计原则
軟體設計原則(七)合成/聚合複用原則(Composite/Aggregate Reuse Principle,CARP)