面向对象编程中的组合与继承:选择与应用指南
组合与继承是常见的面试问题之一。你一定也听说过在设计中要用组合而不是继承。
组合和继承
组合和继承都是面向对象编程的概念。它们与任何特定的编程语言(如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{
}
在编程中,组合优于继承。
无论是组合还是继承,都可以通过不同的方式促进代码的重用。那么该选择哪种呢?如何比较组合和继承关系?你肯定听说过在编程中,应该优先选择组合而非继承。现在让我们看一些能帮助你选择组合和继承关系的理由。
-
继承具有紧密耦合,而组合具有松散耦合。假设我们有以下具有继承关系的类。
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(); } }
-
继承中没有访问控制,而组合中可以限制访问。我们向具有访问子类权限的其他类公开所有超类方法。因此,如果引入新方法或超类中存在安全漏洞,则子类变得脆弱。由于在组合中可以选择使用哪些方法,因此比继承更安全。例如,我们可以使用以下代码在ClassB中为其他类提供ClassA foo()方法的公开。
class ClassB { ClassA classA = new ClassA(); public void foo(){ classA.foo(); } public void bar(){ } }
这是组合优于继承的一个主要优势之一。
-
组合提供了在具有多个子类的情况下调用方法的灵活性。例如,假设我们有以下继承场景。
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(); } }
这将使你有机会根据构造函数中使用的对象选择任何子类。
-
组合相对于继承的另一个好处是测试范围。在组合中进行单元测试更容易,因为我们知道我们从另一个类使用了哪些方法。我们可以为测试目的模拟它,而在继承中,我们严重依赖于超类,不知道会使用超类的哪些方法。因此,我们需要测试超类的所有方法。这是额外的工作,我们需要不必要地进行,这是由于继承而导致的。
这就是有关于组合与继承的全部内容。你已经有足够的理由选择组合而不是继承。只有在确定超类不会被更改的情况下才使用继承,否则选择组合。