合成複用原則詳解篇(附圖解及原始碼例項)

帶你聊技術發表於2023-11-07

來源:mikechen的網際網路架構

透過合成複用,我們可以更加優雅地實現程式碼複用。

合成複用原則解決了繼承在程式碼複用時的相關問題。

本文,主要全面詳解合成複用原則的概念、背景、作用、UML、示例、應用,以及組合和繼承的選型思路。

設計模式的七大原則已全部完結,結合一起看,更易融會貫通:

1. 微信面試:說說里氏替換原則

2. 精通介面隔離原則

3. 依賴倒置原則就看這篇

4. 3 分鐘吃透開閉原則

5. 單一職責原則讓程式碼質量提升100倍

6. 通俗理解迪米特法則

01
  合成複用原則是什麼?

合成複用原則(CRP),又稱為組合/聚合複用原則,英文全稱 Composite Reuse Principle。

合成複用原則要求在實現程式碼複用時,儘量先使用組合或聚合等關聯關係,其次考慮使用繼承關係

合成複用原則的核心思想是:

  • 將不同的類、模組或元件組合在一起,來建立新的類或物件。

  • 儘量不透過繼承已有的類來獲得需要的功能。

 

02  合成複用原則的由來

在早期的物件導向程式設計中,程式碼複用主要用繼承。

透過繼承,一個類可以從另一個類派生,獲得其屬性和方法。

但繼承也有一些缺點:

  • 緊耦合(Tight Coupling):繼承引入了強耦合,使得子類依賴於父類的內部實現細節。當父類發生變化時,子類會受到影響隨之變化。


  • 繼承層次複雜性:隨著繼承層次的增加,程式碼的複雜性也會增加,繼承層次的擴充套件和維護會很複雜。


  • 白箱複用:繼承會將父類的實現細節暴露給子類,父類對子類是透明的,破壞了類的封裝性。


  • 限制了複用的靈活性:從父類繼承而來的實現是靜態的,在編譯時已經定義,所以在執行時不可能發生變化。


為了解決繼承帶來的問題,合成複用原則應運而生。

透過合成複用,我們可以更加優雅地實現程式碼複用。

組合的優點:

  • 低耦合性:程式碼之間的關聯性較小,減少了類之間的緊耦合關係。


  • 黑箱複用:維持了類的封裝性,成分物件的實現細節對新物件不可見。


  • 複用靈活度更高:由於不受繼承關係的限制,可以適應不同的情況和需求。


  • 確保了每個元件的單一職責原則:當一個元件需要進行更改時,只需要關注特定的元件,不需要影響整個繼承層次。


03  組合與繼承如何選型

組合並沒有完全替代繼承,組合與繼承分別適用於不同的場景。

組合通常用於:

  • 需求不斷變化的場景中,同時不受繼承關係的限制。


  • 在類中使用其他類的功能,但不用繼承它們的所有屬性和方法。


繼承通常用於:擴充套件現有類的功能。

是否使用繼承,需要遵循里氏替換原則Coad 法則

3.1  里氏替換原則

里氏替換原則要求子類物件必須能夠替換其父類物件,子類應該保持與父類一致的介面和行為,不應該改變或破壞繼承來的約定。

關於里氏替換原則的介紹,推薦看這篇:

此處我們重點了解 Coad 法則。

3.2  Coad 法則

Coad 法則明確了繼承的具體使用條件:

  • 子類是父類的特殊種類,而不是父類的一個角色。僅 is-a 關係適合繼承,而 has-a 關係應使用組合來描述。


  • 不會出現需要將子類替換成另一個子類的情況。如果不能確定將來是否需要這種替換,就不應使用繼承。


  • 子類應擴充套件而不是替代父類的責任。如果子類需要大規模替代父類的行為,那麼這個類不應是父類的子類。


  • 只有在分類學意義上才可以使用繼承,不要從工具類繼承。


當 Coad 法則中的條件全部被滿足時,才應當使用繼承關係。

這裡要特別注意:

濫用繼承較為常見的錯誤是將 has-a 視為 is-a。

在這裡,is-a 表示一種類是另一種類的一種型別,而 has-a 表示一個類是另一個類的一個組成部分,不是另外一個類的特殊種類。

例如:

“人”是一個型別,“老師”、“組長”、“員工”是“人”的子類,如圖:

合成複用原則詳解篇(附圖解及原始碼例項)

一個人可以同時擁有多個角色,例如,一個人可以同時是“老師”、“組長”、“員工”。

但是,按照繼承的設計邏輯,如果一個人是僱員,就不可能是經理或學生,這顯然是不合理的。

這種設計將“角色”的等級結構和“人”的等級結構混淆了。

正確的設計:

構建一個抽象類“角色”,讓“人”可以同時擁有多個“角色”(組合),“老師”、“組長”、“員工”是“角色”的子類。

合成複用原則詳解篇(附圖解及原始碼例項)

另外,只有兩個類滿足里氏替換原則時,才可能是 is-a 關係。

也就是說,如果兩個類是 has-a 關係,但是設計成了繼承,就必然違反里氏替換原則。


04  合成複用原則的實現示例

假設:

在一個汽車分類管理程式中,汽車有兩種分類方式:

  • 按動力源分類:汽油汽車、電動汽車等;


  • 按顏色分類:白色汽車、黑色汽車和紅色汽車等。


如果使用繼承,就需要同時考慮這兩種分類,將會產生6個組合。

這樣的方式會帶來兩個問題:

  • 導致子類過多。


  • 任何一個分類發生變更,都要修改原始碼,違背了開閉原則。


圖例:

合成複用原則詳解篇(附圖解及原始碼例項)

程式碼:





































































































public abstract class Car{     abstract void run();}

public class ElectricCar extends Car {
   @Override    void run() {        System.out.println("電動汽車");    }
}

public class PetrolCar extends Car {
   @Override    void run() {        System.out.println("汽油汽車");    }
}

public class BlackElectricCar extends ElectricCar{
   public void appearance(){        System.out.print("黑色");        super.run();    }}

public class BlackPetrolCar  extends PetrolCar{    public void appearance(){        System.out.print("黑色");        super.run();    }}

public class RedElectricCar extends ElectricCar {    public void appearance(){        System.out.print("紅色");        super.run();    }}

public class RedPetrolCar  extends PetrolCar{    public void appearance(){        System.out.print("紅色");        super.run();    }}

public class WhiteElectricCar extends ElectricCar{
   public void appearance(){        System.out.print("白色");        super.run();    }}

public class WhitePetrolCar  extends PetrolCar{    public void appearance(){        System.out.print("白色");        super.run();    }}


public class Test {
   public static void main(String[] args) {        RedElectricCar redElectricCar = new RedElectricCar();        redElectricCar.appearance();//紅色電動汽車    }
}

採用組合方式:

  • 先把顏色 Color 抽象為介面,有白色,黑色,紅色三個顏色實現類;


  • 再將 Color 物件組合在汽車 Car 類中。


最終,只需要生成 5 個類,就可以實現上述功能。

並且,後續分類有任何變更,只需要增加實現類,不要去修改原始碼。

圖例:

合成複用原則詳解篇(附圖解及原始碼例項)

程式碼:























































































public abstract class Car{     abstract void run();     Color color;     public Color getColor() {       return color;     }     public void setColor(Color color) {        this.color = color;     }}

public interface Color {      void colorKind();}

public class ElectricCar extends Car {
   @Override    void run() {        System.out.println("電動汽車");    }
}

public class PetrolCar extends Car {
   @Override    void run() {        System.out.println("汽油汽車");    }
}

public class White implements Color{
   @Override    public void colorKind() {        System.out.println("白色");    }
}

public class Black implements Color{
   @Override    public void colorKind() {        System.out.println("黑色");    }
}

public class Red implements Color{
   @Override    public void colorKind() {        System.out.println("紅色");    }
}

public class Test {    public static void main(String[] args) {      ElectricCar electricCar = new ElectricCar();      White color = new White();      electricCar.setColor(color);      electricCar.getColor().colorKind();//白色      electricCar.run();//電動汽車    }}

 

總結  

以上,就是對合成複用原則的概念、背景、作用、UML、示例、應用,以及組合和繼承的選型思路的全面詳解。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024922/viewspace-2993263/,如需轉載,請註明出處,否則將追究法律責任。

相關文章