Effective Java笔记:要么设计继承并提供文档说明,要么禁止继承

这个观点强调的是在使用继承时,设计者必须明确其意图,并为继承行为提供清晰的文档说明来指导子类的扩展。如果无法提供合适的设计和说明,那么禁止继承反而是更好的选择。这项原则通常用于设计复杂或者面向公共 API 的系统,比如开源框架和库的开发中。


核心观点

  1. 继承是一种强大的工具,但也非常危险

    • 继承为子类提供了复用父类代码的能力,同时允许子类重写和扩展的方法。
    • 但它也意味着子类和父类之间形成了紧密的依赖关系。父类的任何实现细节,都可能直接或间接影响子类的行为。
  2. 良好的继承设计需具备文档指导

    • 子类如何扩展父类?什么行为可以被重写?哪些方法只能被调用而不能重写?这些都需要在设计时明确写入文档。
    • 未考虑到子类的行为可能导致继承滥用,让子类破坏父类代码逻辑,或者使子类难以维护(例如意外依赖隐含的逻辑)。
  3. 继承不是默认选项,禁止继承也是一种选择

    • 如果父类的设计无法清晰地说明继承的用途;或者继承的潜在问题(API 被子类重写、范式被破坏等)大于带来的好处,那么设计者应通过技术手段(如将类声明为 final 或者使用组合)禁止继承。

两种策略的实现

我们可以遵循以下两种策略来应用这条原则:“要么设计继承并提供文档说明,要么禁止继承”


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 方法
    }
}

关键点

  1. 使用 Javadoc 来明确说明允许继承的意义及其使用限制。
  2. 通过 final 修饰 power 方法,防止子类直接重写核心逻辑。
  3. 提供一个 protectedcalculatePower 方法作为扩展点,允许子类实现自定义逻辑。

子类场景:

public class ApproximatePowerCalculator extends PowerCalculator {
    @Override
    protected double calculatePower(double base, int exponent) {
        // 简单估算的方法
        return base * exponent; // 示例:简单返回 base * exponent(错误的实现)
    }
}

1.2 限制继承范围

当一个类的实现很容易因子类滥用而出问题时,应尽可能明确哪些部分是封闭的(不可重写),哪些部分开放给子类(可重写)。

  • 将类的关键逻辑封闭(使用 finalprivate)。
  • 提供专门针对扩展的“钩子方法”。

示例 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
}

总结

  1. 要么设计继承并提供文档说明:

    • 允许继承时,需明确设计继承的目标,约束继承的行为(通过 final 或模板方法),并通过文档详细说明子类扩展的重要信息和规则。
  2. 要么禁止继承:

    • 如果一个类不需要继承,或者继承可能导致误用(如工具类、不可变类、单例等),优先禁止它(使用 final 或复合设计)。

这条原则的本质是减少类之间的耦合性,同时增强代码易读性和可维护性。根据需求与设计目标选择允许继承或组合机制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值