天天看點

什麼是繼承?什麼是組合?為何說要多用組合少用繼承?

文章目錄

  • ​​什麼是繼承​​
  • ​​什麼是組合​​
  • ​​繼承群組合有什麼差別和聯系​​
  • ​​為什麼不推薦使用繼承​​
  • ​​組合的好處​​
  • ​​必須使用繼承​​
  • ​​必須使用組合​​
  • ​​參考資料​​

什麼是繼承

繼承就是子類繼承父類的特征和行為,使得子類對象(執行個體)具有父類的執行個體域和方法,或子類從父類繼承方法,使得子類具有父類相同的行為。

/**
 * 動物
 */
public class Animal {
    public void breathing() {
        System.out.println("呼氣...吸氣...");
    }
}

/**
 * 飛行動物
 * 繼承,可以獲得父類屬性和方法
 */
public class FlyingAnimals extends Animal{
    public void filying() {
        System.out.println("飛行...");
    }

    public static void main(String[] args) {
        FlyingAnimals flyingAnimals = new FlyingAnimals();
        flyingAnimals.breathing();
        flyingAnimals.filying();
    }
}      

什麼是組合

組合是通過對現有對象進行拼裝即組合産生新的具有更複雜的功能。

/**
 * 動物
 */
public class Animal {
    public void breathing() {
        System.out.println("呼氣...吸氣...");
    }
}

/**
 * 爬行動物
 * 組合,可以擷取組合對象的屬性和方法
 */
public class Reptilia {

    private Animal animal;

    public Reptilia(Animal animal) {
        this.animal = animal;
    }

    public void crawling() {
        System.out.println("爬行...");
    }
    public void breathing() {
        animal.breathing();
    }


    public static void main(String[] args) {
        Animal animal = new Animal();
        Reptilia reptilia = new Reptilia(animal);
        reptilia.breathing();;
        reptilia.crawling();
    }
}      

繼承群組合有什麼差別和聯系

繼承最大的一個好處就是代碼複用。假如兩個類有一些相同的屬性和方法,我們就可以将這些相同的部分,抽取到父類中,讓兩個子類繼承父類。這樣,兩個子類就可以重用父類中的代碼,避免代碼重複寫多遍。不過,這一點也并不是繼承所獨有的,我們也可以通過其他方式來解決這個代碼複用的問題,比如利用組合關系而不是繼承關系。

繼承是面向對象的四大特性之一,用來表示類之間的 is-a 關系,可以解決代碼複用的問題。雖然繼承有諸多作用,但繼承層次過深、過複雜,也會影響到代碼的可維護性。在這種情況下,我們應該盡量少用,甚至不用繼承。

繼承主要有三個作用:表示 is-a 關系,支援多态特性,代碼複用。而這三個作用都可以通過組合、接口、委托三個技術手段來達成。除此之外,利用組合還能解決層次過深、過複雜的繼承關系影響代碼可維護性的問題。

盡管我們鼓勵多用組合少用繼承,但組合也并不是完美的,繼承也并非一無是處。在實際的項目開發中,我們還是要根據具體的情況,來選擇該用繼承還是組合。如果類之間的繼承結構穩定,層次比較淺,關系不複雜,我們就可以大膽地使用繼承。反之,我們就盡量使用組合來替代繼承。除此之外,還有一些設計模式、特殊的應用場景,會固定使用繼承或者組合。

為什麼不推薦使用繼承

我們定義一個抽象的“鳥”的類,所有鳥都繼承這個類。

AbstractBird 中定義的fly方法并不完全都适用于所有的鳥,比如鴕鳥不會飛,繼承父類之後隻能重寫fly方法,抛出一個異常表示不會飛。

但是這顯然違背了裡氏替換原則,也違背了我們之後要講的最小知識原則(Least Knowledge Principle,也叫最少知識原則或者迪米特法則),暴露不該暴露的接口給外部,增加了類使用過程中被誤用的機率,也徒增了代碼量。

public class AbstractBird {
  //...省略其他屬性和方法...
  public void fly() { //... }
}

public class Ostrich extends AbstractBird { //鴕鳥
  //...省略其他屬性和方法...
  public void fly() {
    throw new UnSupportedMethodException("I can't fly.'");
  }
}      

或許我們可以再更細分一下,分别定義會飛的鳥類 AbstractFlyableBird 和不會飛的鳥類 AbstractUnFlyableBird,繼承關系就變成了:

什麼是繼承?什麼是組合?為何說要多用組合少用繼承?

但是,鳥會不會飛隻是其中一個行為,會不會叫、會不會下蛋……如果每一種行為都定義一個抽象類,那繼承關系會越來越複雜,而且都是強耦合性。

是以,繼承最大的問題就在于:繼承層次過深、繼承關系過于複雜會影響到代碼的可讀性和可維護性。這也是為什麼我們不推薦使用繼承。

組合的好處

上面關于繼承的問題,針對“會飛”這樣一個行為特性,我們可以定義一個 Flyable 接口,隻讓會飛的鳥去實作這個接口。對于會叫、會下蛋這些行為特性,我們可以類似地定義 Tweetable 接口、EggLayable 接口。我們将這個設計思路翻譯成 Java 代碼的話,就是下面這個樣子:

public interface Flyable {
  void fly();
}
public interface Tweetable {
  void tweet();
}
public interface EggLayable {
  void layEgg();
}
public class Ostrich implements Tweetable, EggLayable {//鴕鳥
  //... 省略其他屬性和方法...
  @Override
  public void tweet() { //... }
  @Override
  public void layEgg() { //... }
}
public class Sparrow impelents Flyable, Tweetable, EggLayable {//麻雀
  //... 省略其他屬性和方法...
  @Override
  public void fly() { //... }
  @Override
  public void tweet() { //... }
  @Override
  public void layEgg() { //... }
}      

不過,我們知道,接口隻聲明方法,不定義實作。也就是說,每個會下蛋的鳥都要實作一遍 layEgg() 方法,并且實作邏輯是一樣的,這就會導緻代碼重複的問題。那這個問題又該如何解決呢?

我們可以針對三個接口再定義三個實作類,它們分别是:實作了 fly() 方法的 FlyAbility 類、實作了 tweet() 方法的 TweetAbility 類、實作了 layEgg() 方法的 EggLayAbility 類。然後,通過組合和委托技術來消除代碼重複。具體的代碼實作如下所示:

public interface Flyable {
  void fly();
}
public class FlyAbility implements Flyable {
  @Override
  public void fly() { //... }
}
//省略Tweetable/TweetAbility/EggLayable/EggLayAbility

public class Ostrich implements Tweetable, EggLayable {//鴕鳥
  private TweetAbility tweetAbility = new TweetAbility(); //組合
  private EggLayAbility eggLayAbility = new EggLayAbility(); //組合
  //... 省略其他屬性和方法...
  @Override
  public void tweet() {
    tweetAbility.tweet(); // 委托
  }
  @Override
  public void layEgg() {
    eggLayAbility.layEgg(); // 委托
  }
}      

我們知道繼承主要有三個作用:表示 is-a 關系,支援多态特性,代碼複用。而這三個作用都可以通過其他技術手段來達成。比如 is-a 關系,我們可以通過組合和接口的 has-a 關系來替代;多态特性我們可以利用接口來實作;代碼複用我們可以通過組合和委托來實作。是以,從理論上講,通過組合、接口、委托三個技術手段,我們完全可以替換掉繼承,在項目中不用或者少用繼承關系,特别是一些複雜的繼承關系。

不過,之是以“多用組合少用繼承”這個口号喊得這麼響,隻是因為,長期以來,我們過度使用繼承。其實,組合并不完美,繼承也不是一無是處。隻要我們控制好它們的副作用、發揮它們各自的優勢,在不同的場合下,恰當地選擇使用繼承還是組合,這才是我們所追求的境界。

必須使用繼承

一些特殊的場景要求我們必須使用繼承。如果你不能改變一個函數的入參類型,而入參又非接口,為了支援多态,隻能采用繼承來實作。比如下面這樣一段代碼,其中 FeignClient 是一個外部類,我們沒有權限去修改這部分代碼,但是我們希望能重寫這個類在運作時執行的 encode() 函數。這個時候,我們隻能采用繼承來實作了。

public class FeignClient { // Feign Client架構代碼
  //...省略其他代碼...
  public void encode(String url) { //... }
}

public void demofunction(FeignClient feignClient) {
  //...
  feignClient.encode(url);
  //...
}

public class CustomizedFeignClient extends FeignClient {
  @Override
  public void encode(String url) { //...重寫encode的實作...}
}

// 調用
FeignClient client = new CustomizedFeignClient();
demofunction(client);      

必須使用組合

public class Url {
  //...省略屬性和方法
}

public class Crawler {
  private Url url; // 組合
  public Crawler() {
    this.url = new Url();
  }
  //...
}

public class PageAnalyzer {
  private Url url; // 組合
  public PageAnalyzer() {
    this.url = new Url();
  }
  //..
}