组合 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中收到编译时错误,提示返回类型与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