这个观点强调的是在使用继承时,设计者必须明确其意图,并为继承行为提供清晰的文档说明来指导子类的扩展。如果无法提供合适的设计和说明,那么禁止继承反而是更好的选择。这项原则通常用于设计复杂或者面向公共 API 的系统,比如开源框架和库的开发中。
核心观点
-
继承是一种强大的工具,但也非常危险:
- 继承为子类提供了复用父类代码的能力,同时允许子类重写和扩展的方法。
- 但它也意味着子类和父类之间形成了紧密的依赖关系。父类的任何实现细节,都可能直接或间接影响子类的行为。
-
良好的继承设计需具备文档指导:
- 子类如何扩展父类?什么行为可以被重写?哪些方法只能被调用而不能重写?这些都需要在设计时明确写入文档。
- 未考虑到子类的行为可能导致继承滥用,让子类破坏父类代码逻辑,或者使子类难以维护(例如意外依赖隐含的逻辑)。
-
继承不是默认选项,禁止继承也是一种选择:
- 如果父类的设计无法清晰地说明继承的用途;或者继承的潜在问题(API 被子类重写、范式被破坏等)大于带来的好处,那么设计者应通过技术手段(如将类声明为
final或者使用组合)禁止继承。
- 如果父类的设计无法清晰地说明继承的用途;或者继承的潜在问题(API 被子类重写、范式被破坏等)大于带来的好处,那么设计者应通过技术手段(如将类声明为
两种策略的实现
我们可以遵循以下两种策略来应用这条原则:“要么设计继承并提供文档说明,要么禁止继承”。
1. 设计继承并提供清晰的文档说明
1.1 明确继承的目标
在设计一个允许继承的父类时,需要明确回答以下问题:
- 子类为什么需要继承?
- 哪些方法允许子类重写?
- 子类继承后是否可以修改行为?如果可以,什么是允许的修改范围?
- 子类需要遵循哪些契约(即继承是否有规范)?
示例 1:定义一个允许继承的类
/**
* 用于提供幂操作的一些方法。
* 本类可以被继承,用于对具体的乘幂算法进行扩展。
*
* 注意:
* - 子类必须保证 `power` 方法的输入始终大于或等于 0。
* - 子类可以重写 `calculatePower` 方法来实现自定义的幂运算。
*/
public class PowerCalculator {
/**
* 计算 base^exponent 的幂
* @param base 底数
* @param exponent 指数,必须大于等于 0
* @return 幂运算结果
*/
public final double power(double base, int exponent) {
if (exponent < 0) {
throw new IllegalArgumentException("Exponent must be >= 0.");
}
return calculatePower(base, exponent);
}
/**
* 子类可以覆盖此方法以实现自定义的幂运算逻辑。
* 请确保此方法在 `power` 方法的前提条件范围内运行。
*/
protected double calculatePower(double base, int exponent) {
return Math.pow(base, exponent); // 默认实现使用 Java 内置的 pow 方法
}
}
关键点:
- 使用 Javadoc 来明确说明允许继承的意义及其使用限制。
- 通过
final修饰power方法,防止子类直接重写核心逻辑。 - 提供一个
protected的calculatePower方法作为扩展点,允许子类实现自定义逻辑。
子类场景:
public class ApproximatePowerCalculator extends PowerCalculator {
@Override
protected double calculatePower(double base, int exponent) {
// 简单估算的方法
return base * exponent; // 示例:简单返回 base * exponent(错误的实现)
}
}
1.2 限制继承范围
当一个类的实现很容易因子类滥用而出问题时,应尽可能明确哪些部分是封闭的(不可重写),哪些部分开放给子类(可重写)。
- 将类的关键逻辑封闭(使用
final或private)。 - 提供专门针对扩展的“钩子方法”。
示例 2:模板方法模式(Template Method Pattern)
public abstract class AbstractModel {
/**
* 模板方法,规定了算法的骨架,子类不能修改。
* 始终调用特定步骤(子类通过钩子扩展它们)。
*/
public final void execute() {
step1();
step2();
step3();
}
protected void step1() {
System.out.println("Step 1: Default implementation");
}
protected abstract void step2(); // 由子类实现
private void step3() {
System.out.println("Step 3: This is a private step.");
}
}
扩展方式:
public class CustomModel extends AbstractModel {
@Override
protected void step2() {
System.out.println("Step 2: My custom implementation");
}
}
public class Main {
public static void main(String[] args) {
AbstractModel model = new CustomModel();
model.execute();
// 输出:
// Step 1: Default implementation
// Step 2: My custom implementation
// Step 3: This is a private step.
}
}
通过模板对算法的步骤进行固定,确保子类只能修改被允许的部分,例如 step2,而其他部分对子类是封闭的。
2. 禁止继承
当一个类不适合被子类化时,可以通过以下方式禁止继承:
2.1 使用 final 关键字
通过将一个类声明为 final,可以禁止任何类从这个类继承。
public final class UtilityClass {
public static int add(int a, int b) {
return a + b;
}
}
对于工具类(跟逻辑无关,只保存一些静态方法),禁止继承是一种推荐的设计方法。
2.2 将构造函数声明为 private
通过使构造函数 private 并公开一个静态工厂方法,可以完全限制从类外实例化或继承。
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
// 私有的构造函数
}
public static Singleton getInstance() {
return INSTANCE;
}
}
这种设计确保了 Singleton 类完全不能被继承,因为外界无法访问它的构造函数。
2.3 使用复合代替继承
在很多情况下,复合是一种可以代替继承的更安全的设计模式(见之前“复合优于继承”的讨论)。通过组合其他类而非继承,可以提高类的可维护性和封装性。
class Car {
private Engine engine; // 使用复合
// ... methods
}
总结
-
要么设计继承并提供文档说明:
- 允许继承时,需明确设计继承的目标,约束继承的行为(通过
final或模板方法),并通过文档详细说明子类扩展的重要信息和规则。
- 允许继承时,需明确设计继承的目标,约束继承的行为(通过
-
要么禁止继承:
- 如果一个类不需要继承,或者继承可能导致误用(如工具类、不可变类、单例等),优先禁止它(使用
final或复合设计)。
- 如果一个类不需要继承,或者继承可能导致误用(如工具类、不可变类、单例等),优先禁止它(使用
这条原则的本质是减少类之间的耦合性,同时增强代码易读性和可维护性。根据需求与设计目标选择允许继承或组合机制。
308

被折叠的 条评论
为什么被折叠?



