合成复用原则:优先组合、少用继承

说到设计原则,合成复用原则可能是最容易被忽视、却又最影响代码质量的一个。我刚开始写Java那几年,看到"继承"两个字就两眼放光——多好的复用方式啊!父类写一次,子类随便用。直到有一次,我在一个支付系统里被继承坑得体无完肤……

嗯,今天我们就来聊聊这个原则。说白了就一句话:能用组合就别用继承

为什么继承会出问题?

继承本身不是坏事。但很多人把它当成了"万能复用工具"。你想想看,继承有几个硬伤:

  • 破坏封装:子类能访问父类的内部细节。父类改个方法,子类可能就崩了。
  • 强耦合:子类和父类绑死了。运行时想换行为?没门。
  • 继承层次深了,代码像蜘蛛网:我见过一个项目,继承链有6层。改底层一个方法,上面全得回归测试。

核心观点:继承是"白盒复用",组合是"黑盒复用"。白盒意味着你暴露了内部,黑盒意味着你只关心接口。

组合的优势在哪里?

组合的思路很简单:把功能拆成小零件,然后拼起来。每个零件只管自己的事,互不干扰。

我在项目中遇到过这样一个场景:需要做一个消息推送系统,支持短信、邮件、App推送。一开始有人用继承设计:

// 继承方式——不推荐
public abstract class Notifier {
    public abstract void send(String message);
}

public class SmsNotifier extends Notifier {
    @Override
    public void send(String message) {
        // 发短信
    }
}

public class EmailNotifier extends Notifier {
    @Override
    public void send(String message) {
        // 发邮件
    }
}

看起来还行对吧?但需求变了——要支持"同时发短信和邮件"。怎么办?再写一个 SmsAndEmailNotifier?那如果还要支持"短信+App推送"呢?组合爆炸了。

用组合来重构:

// 组合方式——推荐
public interface SendStrategy {
    void send(String message);
}

public class SmsStrategy implements SendStrategy {
    @Override
    public void send(String message) {
        // 发短信
    }
}

public class EmailStrategy implements SendStrategy {
    @Override
    public void send(String message) {
        // 发邮件
    }
}

public class CompositeStrategy implements SendStrategy {
    private List<SendStrategy> strategies = new ArrayList<>();

    public void addStrategy(SendStrategy strategy) {
        strategies.add(strategy);
    }

    @Override
    public void send(String message) {
        for (SendStrategy s : strategies) {
            s.send(message);
        }
    }
}

看到了吗?组合方式下,每种发送方式是一个独立的策略。想组合?直接往 CompositeStrategy 里加就行。不用改任何已有代码。

个人习惯:我写新功能时,会先问自己一句"这个行为未来会不会变?"如果答案是"会",我就用组合。如果答案是"不会",而且确实有 is-a 关系,我才考虑继承。

实战:组合优于继承案例

我们来看一个更完整的例子。假设你要做一个订单计算器,支持不同的折扣策略。

继承方式:

public abstract class OrderCalculator {
    public abstract double calculate(double amount);
}

public class NoDiscountCalculator extends OrderCalculator {
    @Override
    public double calculate(double amount) {
        return amount;
    }
}

public class PercentageDiscountCalculator extends OrderCalculator {
    private double percent;

    public PercentageDiscountCalculator(double percent) {
        this.percent = percent;
    }

    @Override
    public double calculate(double amount) {
        return amount * (1 - percent / 100);
    }
}

public class FixedDiscountCalculator extends OrderCalculator {
    private double discount;

    public FixedDiscountCalculator(double discount) {
        this.discount = discount;
    }

    @Override
    public double calculate(double amount) {
        return Math.max(0, amount - discount);
    }
}

现在需求来了:先打8折,再减20元。继承方式下,你得再写一个类:

public class PercentageThenFixedCalculator extends OrderCalculator {
    // 组合逻辑写死在这个类里
    // 如果以后要"先减20再打8折"?再写一个类……
}

用组合方式:

public interface DiscountStrategy {
    double apply(double amount);
}

public class NoDiscount implements DiscountStrategy {
    @Override
    public double apply(double amount) {
        return amount;
    }
}

public class PercentageDiscount implements DiscountStrategy {
    private double percent;

    public PercentageDiscount(double percent) {
        this.percent = percent;
    }

    @Override
    public double apply(double amount) {
        return amount * (1 - percent / 100);
    }
}

public class FixedDiscount implements DiscountStrategy {
    private double discount;

    public FixedDiscount(double discount) {
        this.discount = discount;
    }

    @Override
    public double apply(double amount) {
        return Math.max(0, amount - discount);
    }
}

// 组合器——核心
public class CompositeDiscount implements DiscountStrategy {
    private List<DiscountStrategy> strategies = new ArrayList<>();

    public CompositeDiscount add(DiscountStrategy strategy) {
        strategies.add(strategy);
        return this;
    }

    @Override
    public double apply(double amount) {
        double result = amount;
        for (DiscountStrategy s : strategies) {
            result = s.apply(result);
        }
        return result;
    }
}

使用起来非常灵活:

// 先打8折,再减20
DiscountStrategy strategy = new CompositeDiscount()
    .add(new PercentageDiscount(20))
    .add(new FixedDiscount(20));

double finalPrice = strategy.apply(100); // 100 * 0.8 = 80, 80 - 20 = 60

想换顺序?调换 add 的顺序就行。想加新策略?写一个新类实现 DiscountStrategy 接口,然后加进去。完全不用改已有代码。

我曾经踩过的坑:有一次我把折扣逻辑写死在继承体系里,结果产品经理说"我们要支持满减和打折叠加,而且顺序可配"。我一看代码,继承链已经三层了,改起来牵一发动全身。最后花了三天重构,换成组合模式。从那以后,我再也不敢随便用继承了。

什么时候可以用继承?

也不是说继承就完全不能用。我个人觉得,以下情况可以考虑:

  • 明确的 is-a 关系:比如 Dog extends Animal,狗确实是动物。
  • 子类不会改变父类行为:只是扩展,不是覆盖。
  • 继承层次不超过两层:超过两层,代码可读性就开始下降了。

但即使满足这些条件,我也会优先考虑组合。为什么?因为组合更灵活,更容易测试,更容易维护。

知识体系图

下面这张图展示了合成复用原则的核心逻辑:

合成复用原则核心逻辑 合成复用原则 继承(白盒复用) 破坏封装 强耦合,难以扩展 继承层次深,维护困难 组合(黑盒复用) 保护封装 松耦合,灵活组合 易于测试和维护 结论:优先组合,少用继承

总结

合成复用原则不是什么高深的理论。它就是一个朴素的道理:别把代码绑死,留点余地

我见过太多项目,一开始用继承觉得挺爽,半年后需求一变,改代码改到崩溃。而用组合的项目,虽然初期多写几个接口和类,但后期扩展起来非常顺畅。

记住一句话:继承是"我是谁",组合是"我能做什么"。在大多数业务场景下,"我能做什么"比"我是谁"重要得多。

最后给个实用建议:写代码前,先画一下类图。如果发现继承层次超过两层,或者子类需要覆盖父类的方法,果断换成组合。这个习惯,能帮你省下大量重构时间。

公众号:蓝海资料掘金营,微信deep3321