从一次支付渠道扩展需求,看懂"多用组合少用继承"到底在说什么

「设计模式与范式」系列 Day03

写在前面

Day02 打完了四大特性的地基,这篇不再逐个讲概念,而是用一个真实场景把接口、抽象类、组合、继承这几个知识点串起来:给电商系统加支付渠道。这是个几乎所有后端都会遇到的需求,也是"继承一开始很好用,后来突然绷不住"的典型案例。


一、是什么:接口和抽象类的核心区别

抽象类 接口
能否实例化 不能,只能被继承 不能,只能被实现
包含什么 属性 + 方法(可以有实现,也可以是抽象方法) 只能声明方法,不能有实现,不能有属性
表达的关系 is-a,解决代码复用 has-a,表达一组行为约定

一句话记住区别:抽象类回答"这是什么",接口回答"这个东西能做什么"。带着这个区别看下面的案例。


二、为什么:继承一开始好用,后来为什么绷不住

第一版:一个抽象类,很优雅

需求只有支付宝、微信两个渠道时,模板方法式的继承设计非常合适------支付流程(校验、扣款、通知商户)是固定的,只有"具体怎么扣款"这一步因渠道而异:

java 复制代码
public abstract class AbstractPaymentChannel {
    public final void pay(Order order) {
        validate(order);
        doPay(order);          // 唯一因渠道而异的一步
        notifyMerchant(order);
    }
    protected void validate(Order order) { /* 通用校验逻辑 */ }
    protected abstract void doPay(Order order);
    protected void notifyMerchant(Order order) { /* 通用通知逻辑 */ }
}

public class AlipayChannel extends AbstractPaymentChannel {
    protected void doPay(Order order) { /* 调支付宝SDK */ }
}

这时候用继承没有任何问题------流程稳定、继承层次只有一层,这正是 Day02 说的"继承结构稳定、层次浅,可以放心用"的场景。

第二版:加了退款、分账需求,继承开始爆炸

后来产品提了新需求:支付宝、微信要支持退款;银联不支持退款但支持分账;某个新渠道两者都要支持。如果继续在继承体系里加方法,马上会撞上 Day02 讲过的老问题------父类要不要给所有子类都加上 refund()split()

  • 不加:每个需要的子类各自定义方法,接口不统一,调用方没法用同一种方式判断"这个渠道支不支持退款"
  • 硬加到父类:银联、不支持退款的渠道也被迫继承一个用不上的方法,这就是 Day02 说的"父类里放子类才需要的方法"这个反模式
  • 用继承体系表达"既支持退款又支持分账":Java 单继承,走不通,只能搞出 RefundableAndSplittableChannel 这类为了应付排列组合而生的怪异子类

问题的根源是:退款、分账是"渠道具备的能力",这是 has-a 关系,不是"渠道是什么"的 is-a 关系------用继承去表达能力组合,天生就是错的工具。

第三版:把能力拆成接口,用组合而不是继承拼装

java 复制代码
public interface PaymentChannel {
    void pay(Order order);
}
public interface Refundable {
    void refund(Order order);
}
public interface Splittable {
    void split(Order order);
}

// 一个类可以实现多个接口,能力想要几个就实现几个,不受单继承限制
public class AlipayChannel implements PaymentChannel, Refundable {
    public void pay(Order order) { /* ... */ }
    public void refund(Order order) { /* ... */ }
}
public class UnionPayChannel implements PaymentChannel, Splittable {
    public void pay(Order order) { /* ... */ }
    public void split(Order order) { /* ... */ }
}

原来那段"校验 + 通知商户"的通用流程怎么办?不再靠继承一个基类,而是组合一个流程编排对象,各渠道实现类内部持有它、委托给它执行:

java 复制代码
public class AlipayChannel implements PaymentChannel, Refundable {
    private final PaymentFlowTemplate flow = new PaymentFlowTemplate();
    public void pay(Order order) {
        flow.execute(order, this::doPay);   // 通用流程委托给flow,doPay才是自己的事
    }
    private void doPay(Order order) { /* 调支付宝SDK */ }
    public void refund(Order order) { /* ... */ }
}

调用方全程只依赖 PaymentChannel 这个接口:

java 复制代码
public class PaymentService {
    private final Map<String, PaymentChannel> channels; // 依赖接口,不关心具体实现
    public void pay(String channelCode, Order order) {
        channels.get(channelCode).pay(order);
    }
}

新增渠道、给某个渠道加退款能力,PaymentService 这段代码一行都不用改------这就是"基于接口而非实现编程"的价值:调用方依赖的是稳定契约,实现细节可以自由增删组合,不会传导到调用方。

现实世界里同样的思路

这不是我编出来的套路,JDK 和 Spring 里到处都是这个模式:List<String> list = new ArrayList<>();------声明变量类型永远用接口 List 而不是具体的 ArrayList,换成 LinkedList 完全不影响后面的代码;Spring 的 JdbcTemplate 只依赖 DataSource 这一个接口,底层连接池换成 HikariCP 还是 Druid,JdbcTemplate 的代码根本不用改一行。


三、怎么用:两条可以直接套用的判断标准

  • 要不要抽接口:只有当某个功能存在多种实现、或者未来有被替换/组合的可能时才值得抽象出接口(案例里的退款、分账能力);只有一种实现且未来基本不变的,直接用实现类,强行加接口只是徒增复杂度。
  • 该用继承还是组合:继承结构稳定、层次浅、关系不复杂(案例第一版),放心用继承;一旦出现"能力交叉组合"的迹象(案例第二版),就是继承要绷不住的信号,优先拆成接口用组合。设计模式里也有约定:模板方法模式固定用继承;装饰器模式、策略模式、组合模式固定用组合,后面写到这几个模式时还会再遇到这个案例的影子。

四、面试追问

Q1:接口和抽象类最核心的区别是什么?

抽象类是对成员变量和方法的抽象,表达 is-a 关系,目的是代码复用;接口只对方法进行抽象,不能有属性和实现,表达 has-a 关系(具备某种行为能力),目的是隔离约定和实现,解决解耦问题。

Q2:什么时候应该用继承,什么时候应该改成接口 + 组合?

继承结构稳定、层次浅、关系不复杂时可以放心用继承。一旦出现"某些子类需要能力 A,某些需要能力 B,还有子类同时需要 A 和 B"这种能力交叉组合的需求,继承(尤其是 Java 单继承)就会力不从心,应该把能力拆成独立接口,用组合把需要的能力装配到具体类上。

Q3:怎么用一个普通类模拟出抽象类和接口的效果?

核心手法是"私有化构造方法 + 方法直接抛异常占位":模拟抽象类时把构造方法设为 protected,让外部拿不到裸实例,抽象方法体里 throw new UnsupportedOperationException() 强制子类重写;模拟接口时只声明方法(同样用异常占位),不定义任何属性。这只是帮助理解两者本质的思维实验,实际项目里直接用 abstract class/interface 语法,编译期就能强制约束,比运行时抛异常安全得多。

Q4:是不是每个类都应该定义一个对应的接口?

不是。接口的价值在于让调用方依赖稳定契约、不被具体实现绑死,只有存在多种实现或未来可能变化时才值得抽象。功能单一且稳定的场景,直接用实现类,强行加接口只是不必要的开发成本。


下一篇预告

Day04 是面向对象思想的实战篇:拿一个真实需求做一次完整的设计走查,顺带讲清楚贫血模型与充血模型该怎么选。

相关推荐
geovindu2 小时前
CSharp:Condition Variable Pattern
后端·设计模式·c#·.net·.netcore·条件变量模式·同步型模式
geovindu2 小时前
sql: Data Modeling Patterns using mysql
sql·mysql·设计模式·数据库开发
sarasuki21 小时前
失败重试:Agent 中指数退避的正确姿势
人工智能·设计模式·agent
Zane199421 小时前
为什么你写的 Java 代码"看着面向对象、实际是面向过程"?
设计模式
geovindu1 天前
sql: Data Modeling Patterns
数据库·sql·设计模式·sqlserver
YHL3 天前
🎯 JavaScript 单例模式(Singleton Pattern)—— 从理论到实战
javascript·设计模式
2401_868534783 天前
网络环境规划核心考点全梳理
网络·设计模式
feiyu_gao3 天前
Cocreation Framework:一套给所有人的 AI 协作思考系统
设计模式·aigc·ai编程
善良勤劳勇敢而又聪明的老杨4 天前
【AI编程系列】MCP 常用设计模式解读
设计模式·ai编程