2. 继承基础
继承是一种强大但容易被过度使用和误用的机制。
简单来说,通过继承,一个基类(也称为基类型)定义了特定类型共有的状态和行为,并让子类(也称为子类型)提供这些状态和行为的专门版本。
为了清晰地展示如何使用继承,我们创建一个简单的例子:一个基类Person
,它定义了人的通用字段和方法,而子类Waitress
和Actress
则提供额外的、更细化的方法实现。
以下是Person
类:
public class Person {
private final String name;
// 其他字段、标准构造函数、getter
}
以下是子类:
public class Waitress extends Person {
public String serveStarter(String starter) {
return "Serving a " + starter;
}
// 其他方法/构造函数
}
public class Actress extends Person {
public String readScript(String movie) {
return "Reading the script of " + movie;
}
// 其他方法/构造函数
}
此外,我们创建一个单元测试来验证Waitress
和Actress
类的实例也是Person
的实例,从而在类型层面满足“is-a”条件:
@Test
public void givenWaitressInstance_whenCheckedType_thenIsInstanceOfPerson() {
assertThat(new Waitress("Mary", "[email protected]", 22))
.isInstanceOf(Person.class);
}
@Test
public void givenActressInstance_whenCheckedType_thenIsInstanceOfPerson() {
assertThat(new Actress("Susan", "[email protected]", 30))
.isInstanceOf(Person.class);
}
这里必须强调继承的语义方面。除了重用Person
类的实现,我们还建立了基类型Person
与子类型Waitress
和Actress
之间定义明确的“is-a”关系。服务员和女演员实际上都是人。
这可能会让我们思考:在哪些用例中继承是正确的方法?
✅ 如果子类满足“is-a”条件,并且主要在类层次结构中提供附加功能,那么继承就是可行的方式。
当然,只要被重写的方法保持里氏替换原则所提倡的基类型/子类型可替换性,方法重写是允许的。
此外,我们应该记住,子类继承了基类型的API,这在某些情况下可能过度或根本不理想。
否则,我们应该使用组合。
3. 设计模式中的继承
虽然共识是尽可能优先使用组合而非继承,但在一些典型用例中,继承仍有其用武之地。
3.1. 层超类型模式
在这种情况下,我们使用继承将公共代码移动到基类(超类型),按层进行。
以下是该模式在领域层的基本实现:
public class Entity {
protected long id;
// setter
}
public class User extends Entity {
// 其他字段和方法
}
我们可以对系统中的其他层(如服务层和持久层)应用相同的方法。
3.2. 模板方法模式
在模板方法模式中,我们可以使用基类定义算法的不变部分,然后在子类中实现可变部分:
public abstract class ComputerBuilder {
public final Computer buildComputer() {
addProcessor();
addMemory();
}
public abstract void addProcessor();
public abstract void addMemory();
}
public class StandardComputerBuilder extends ComputerBuilder {
@Override
public void addProcessor() {
// 方法实现
}
@Override
public void addMemory() {
// 方法实现
}
}
4. 组合基础
组合是面向对象编程提供的另一种重用实现的机制。
简而言之,组合允许我们建模由其他对象组成的对象,从而在它们之间定义“has-a”关系。
此外,组合是关联关系中最强的形式,这意味着当一个对象被销毁时,组成或包含它的对象也会被销毁。
为了更好地理解组合的工作原理,假设我们需要处理表示计算机的对象。
计算机由不同的部分组成,包括微处理器、内存、声卡等,因此我们可以将计算机及其每个部分建模为单独的类。
以下是Computer
类的简单实现:
public class Computer {
private Processor processor;
private Memory memory;
private SoundCard soundCard;
// 标准getter/setter/构造函数
public Optional<SoundCard> getSoundCard() {
return Optional.ofNullable(soundCard);
}
}
以下类建模了微处理器、内存和声卡(为简洁起见省略接口):
public class StandardProcessor implements Processor {
private String model;
// 标准getter/setter
}
public class StandardMemory implements Memory {
private String brand;
private String size;
// 标准构造函数、getter、toString
}
public class StandardSoundCard implements SoundCard {
private String brand;
// 标准构造函数、getter、toString
}
不难理解为什么优先选择组合而非继承。在任何场景下,只要能够在给定类与其他类之间建立语义上正确的“has-a”关系,组合就是正确的选择。
在上面的例子中,Computer
类与建模其部件的类之间满足“has-a”条件。
还值得注意的是,在这种情况下,包含的Computer
对象拥有被包含对象的所有权,当且仅当这些对象不能在另一个Computer
对象中重用。如果可以重用,我们使用的是聚合而非组合,其中不隐含所有权。
5. 不使用抽象的组合
或者,我们可以通过硬编码Computer
类的依赖关系来定义组合关系,而不是在构造函数中声明它们:
public class Computer {
private StandardProcessor processor
= new StandardProcessor("Intel I3");
private StandardMemory memory
= new StandardMemory("Kingston", "1TB");
// 其他字段/方法
}
⚠️ 当然,这将是一个僵化、紧耦合的设计,因为我们会使Computer
类强依赖于Processor
和Memory
的特定实现。
我们不会利用接口和依赖注入提供的抽象级别。
基于接口的初始设计,我们得到了一个松耦合的设计,也更容易测试。
6. 结论
在本文中,我们学习了Java中继承和组合的基础知识,并深入探讨了两种关系类型(“is-a” vs “has-a”)之间的差异。
关键要点总结:
- ✅ 继承建立“is-a”关系,适用于子类是基类的特化场景
- ✅ 组合建立“has-a”关系,适用于对象由其他对象组成的场景
- ❌ 避免过度使用继承,防止API污染和紧耦合
- ⚠️ 组合时优先使用接口和依赖注入,而非硬编码依赖
一如既往,本教程中显示的所有代码示例都可以在GitHub上找到。