从一个电商订单说起
假设你在写一个电商系统的订单处理模块。一开始,所有订单都走微信支付。你很自然地写了个 Order 类,然后让 WeChatPayOrder 继承它,加个支付方法。代码跑得挺好。
可没过多久,产品说要支持支付宝、银行卡,甚至未来可能接入数字货币。你开始头疼了。如果继续用继承,是不是得写一堆子类:AlipayOrder、BankOrder、DigitalCurrencyOrder?每个都重复一套流程,只改一点点支付逻辑。
类继承的甜蜜陷阱
继承听起来很直观:父子关系,代码复用。但问题就出在这“复用”上。你复用的不只是功能,还绑定了结构和行为。一旦父类变了,所有子类都得跟着动。就像你家装修,客厅改布局,结果厨房也莫名其妙被重装了一遍。
再看代码:
class Order {
public void process() {
validate();
pay();
sendNotification();
}
protected abstract void pay();
}
class WeChatPayOrder extends Order {
protected void pay() {
// 微信支付逻辑
}
}
看着清爽,但每加一种支付方式,就得加一个类。更糟的是,测试也得跟着翻倍。你想换种支付方式?不好意思,得重新实例化一个类,甚至改构造逻辑。
依赖注入:把选择权交出去
换个思路。订单本身不关心你怎么付钱,它只关心“能付成”。那不如把支付方式当成一个零件,组装进去。
这时候依赖注入就派上用场了。你定义一个 PaymentService 接口,微信、支付宝各实现一份。订单类初始化时,外面把具体的支付服务塞进来。
interface PaymentService {
void pay(double amount);
}
class Order {
private PaymentService paymentService;
public Order(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void process() {
validate();
paymentService.pay(totalAmount);
sendNotification();
}
}
你看,订单类现在干净了。加新支付方式?写个新实现,传进去就行。测试也方便,mock 一个假支付服务,立马跑单元测试。
场景决定选择
但这不是说继承就没用了。如果你在做一个图形编辑器,形状有圆形、矩形、三角形,它们都有面积、周长这些共性,用继承就很自然。因为这是典型的“是什么”的关系——矩形是一个形状。
而支付方式和订单之间是“用什么”的关系——订单用某种支付方式,而不是“订单是一种支付方式”。这种组合关系,更适合依赖注入。
再打个比方:继承像定制手机,所有功能焊死在主板上;依赖注入像模块化手机,摄像头、电池、存储都能换。现在大家为啥偏爱后者?因为变化太快,没人想为换个闪光灯就扔掉整台手机。
灵活性背后的代价
依赖注入也不是银弹。配置复杂了,尤其是项目一大,一堆 service、repository 层层注入,新手一看懵。Spring 框架帮你管理这些,但理解成本也上去了。
而继承简单直接,一眼看懂谁是谁的子类。小项目、稳定需求下,反而更省事。就像修自行车,补胎用胶水一粘就行,没必要上3D打印定制件。
设计的本质是取舍
真正重要的不是用哪个技术,而是搞清楚对象之间的关系。是“属于”还是“使用”?是稳定共性还是易变部分?
继承适合提取稳定不变的共性,比如用户基类里的 ID、创建时间。依赖注入适合封装可能变化的行为,比如日志输出到文件还是远程服务器。
高手写代码,不是背套路,而是像搭积木,知道哪块该固定,哪块该留插槽。类继承给你结构,依赖注入给你弹性。什么时候用哪个,取决于你想让系统哪里硬,哪里软。