面向对象编程中的组合与继承:选择与应用指南

组合与继承是常见的面试问题之一。你一定也听说过在设计中要用组合而不是继承。

组合和继承

组合和继承都是面向对象编程的概念。它们与任何特定的编程语言(如Java)无关。在我们对比组合与继承的编程实现之前,让我们对它们进行简要定义。

组合

组合是面向对象编程中的设计技术,用于实现对象之间的”拥有关系”。在Java中,可以通过使用其他对象的实例变量来实现组合。例如,在Java面向对象编程中,一个拥有职位的人可以被实现如下。

package com.Olivia.composition;

public class Job {
// 变量、方法等
}
package com.Olivia.composition;

public class Person {

    // 组合关系 - 拥有关系
    private Job job;

    // 变量、方法、构造函数等面向对象编程内容
}

继承

继承是面向对象编程中实现对象之间的”is-a”关系的设计技术。在Java中,继承是通过extends关键字来实现的。例如,在Java编程中,Cat是Animal的关系可以如下实现。

package com.Olivia.inheritance;
 
public class Animal {
// 变量、方法等
}
package com.Olivia.inheritance;
 
public class Cat extends Animal{
}

在编程中,组合优于继承。

无论是组合还是继承,都可以通过不同的方式促进代码的重用。那么该选择哪种呢?如何比较组合和继承关系?你肯定听说过在编程中,应该优先选择组合而非继承。现在让我们看一些能帮助你选择组合和继承关系的理由。

  1. 继承具有紧密耦合,而组合具有松散耦合。假设我们有以下具有继承关系的类。

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

    为了简化,我们将超类和子类都放在同一个包中,但它们通常会在不同的代码库中。可能会有许多类扩展超类ClassA。这种情况的一个非常常见的例子是扩展异常类。现在假设ClassA的实现发生了以下变化,添加了一个新方法bar()。

    package com.Olivia.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;
    
        Test(Abs o){
            this.obj = o;
        }
    
        public void foo(){
            this.obj.foo();
        }
    }
    

    这将使你有机会根据构造函数中使用的对象选择任何子类。

  4. 组合相对于继承的另一个好处是测试范围。在组合中进行单元测试更容易,因为我们知道我们从另一个类使用了哪些方法。我们可以为测试目的模拟它,而在继承中,我们严重依赖于超类,不知道会使用超类的哪些方法。因此,我们需要测试超类的所有方法。这是额外的工作,我们需要不必要地进行,这是由于继承而导致的。

这就是有关于组合与继承的全部内容。你已经有足够的理由选择组合而不是继承。只有在确定超类不会被更改的情况下才使用继承,否则选择组合。

bannerAds