組合 vs 繼承

組合 vs 繼承是經常被問到的面試問題之一。你可能也聽過要使用組合而不是繼承。

組合 vs 繼承

組合和繼承都是面向對象編程的概念。它們與任何特定的編程語言(如Java)無關。在我們進行程序上的組合與繼承比較之前,讓我們先快速定義一下它們。

組合

組合是面向對象編程中實現對象之間有一個關係的設計技術。在Java中,通過使用其他對象的實例變量來實現組合。例如,在Java面向對象編程中,具有工作的人可以像下面這樣實現。

package com.journaldev.composition;

public class Job {
// 變量,方法等
}
package com.journaldev.composition;

public class Person {

    // 組合有一個關係
    private Job job;

    // 變量,方法,構造函數等。面向對象

繼承

繼承是物件導向程式設計中實現物件間”is-a”關係的設計技術。在Java中,繼承是使用extends關鍵字實現的。例如,在java編程中,Cat是一個Animal關係,將被實現如下所示。

package com.journaldev.inheritance;
 
public class Animal {
// 變數,方法等

}
package com.journaldev.inheritance;
 
public class Cat extends Animal{
}

組合優於繼承

組合和繼承都通過不同的方法促進代碼重用。那麼該選擇哪一種呢?如何比較組合和繼承。你一定聽過在編程中應該優先選擇組合而不是繼承。讓我們看看一些原因,這將幫助你選擇組合還是繼承。

  1. 繼承是緊密耦合的,而組合則是鬆散耦合的。假設我們有以下具有繼承關係的類別。

    package com.journaldev.java.examples;
    
    public class ClassA {
    
    	public void foo(){	
    	}
    }
    
    class ClassB extends ClassA{
    	public void bar(){
    		
    	}
    }
    

    為了簡單起見,我們將超類和子類都放在單個包中。但通常它們會在不同的代碼庫中。可能有許多類擴展了超類 ClassA。這種情況的一個非常常見的例子是擴展 Exception 類。現在假設 ClassA 的實現如下所示,添加了一個新方法 bar()。

    package com.journaldev.java.examples;
    
    public class ClassA {
    
    	public void foo(){	
    	}
    	
    	public int bar(){
    		return 0;
    	}
    }
    

    一旦開始使用新的 ClassA 實現,您將在 ClassB 中獲得編譯時錯誤,如 The return type is incompatible with ClassA.bar()。解決方法是更改超類或子類的 bar() 方法使它們兼容。如果您使用組合而不是繼承,則永遠不會遇到此問題。使用組合的 ClassB 實現的一個簡單示例可能如下所示。

    class ClassB{
    	ClassA classA = new ClassA();
    	
    	public void bar(){
    		classA.foo();
    		classA.bar();
    	}
    }
    
  2. 繼承中沒有訪問控制,而在組合中可以限制訪問。我們向所有具有對子類訪問權限的其他類公開所有超類方法。因此,如果在超類中引入新方法或存在安全漏洞,子類就變得容易受到攻擊。由於在組合中我們可以選擇使用哪些方法,因此比繼承更安全。例如,我們可以使用以下代碼在 ClassB 中向其他類提供 ClassA foo() 方法的訪問權限。

    class ClassB {
    	
    	ClassA classA = new ClassA();
    	
    	public void foo(){
    		classA.foo();
    	}
    	
    	public void bar(){	
    	}
    	
    }
    

    這是組合優於繼承的一個主要優勢之一。

  3. 組合在多個子類情況下提供了調用方法的靈活性。例如,假設我們有以下繼承情況。

    abstract class Abs {
    	abstract void foo();
    }
    
    public class ClassA extends Abs{
    
    	public void foo(){	
    	}
    	
    }
    
    class ClassB extends Abs{
    		
    	public void foo(){
    	}
    	
    }
    
    class Test {
    	
    	ClassA a = new ClassA();
    	ClassB b = new ClassB();
    
    	public void test(){
    		a.foo();
    		b.foo();
    	}
    }
    

    那麼如果有更多的子類,組合會通過為每個子類有一個實例使我們的代碼變得難看嗎?不,我們可以像下面這樣重寫Test類。

    class Test {
    	Abs obj = null;
    	
    	Test1(Abs o){
    		this.obj = o;
    	}
    	
    	public void foo(){
    		this.obj.foo();
    	}
    
    }
    

    這將使您能夠根據構造函數中使用的對象使用任何子類。

  4. 組合相對於繼承的另一個好處是測試範圍。在組合中進行單元測試很容易,因為我們知道我們從另一個類別使用了哪些方法。我們可以模擬它進行測試,而在繼承中,我們嚴重依賴於超類,並不知道超類的所有方法將被使用。因此,我們將不得不測試超類的所有方法。這是額外的工作,我們不必因為繼承而做這些不必要的工作。

這就是組合與繼承的比較。您已經有足夠的理由選擇組合而不是繼承。僅當您確定超類不會更改時才使用繼承,否則請使用組合。

Source:
https://www.digitalocean.com/community/tutorials/composition-vs-inheritance