本文目录导读:

在 PHP 项目中,继承 和 组合 是面向对象设计的两种核心复用机制,选择哪一个,直接影响到代码的可维护性、灵活性和扩展性。
业界有一个广为流传的指导原则:“优先使用组合而非继承” (Favor composition over inheritance),但这并不意味着继承就“一无是处”,关键在于理解它们的本质和适用场景。
以下是一个详细的对比与分析,帮助你在 PHP 项目中做出取舍。
核心概念速览
| 特性 | 继承 | 组合 |
|---|---|---|
| 关系 | is-a (是一个) |
has-a (有一个) |
| 耦合度 | 高(子类与父类强绑定) | 低(依赖接口或抽象类) |
| 复用粒度 | 类级别复用 | 对象/接口级别复用 |
| 运行时改变 | 静态(编译期决定) | 动态(运行时可替换) |
| 层次结构 | 单线/树状,容易产生深层继承链 | 扁平、灵活,通过接口组合行为 |
| PHP 特性 | 单继承(一个子类只能有一个父类) | 无限制(一个类可以组合任意多个对象) |
什么时候用 继承?
继承最符合直觉,但滥用是代码腐化的开始,适合继承的场景通常满足:
强 is-a 关系,且子类不会改变父类核心行为
- 父类提供明确的“骨架”逻辑(模板方法模式)。
- 子类只需扩展或微调特定步骤。
反例:
Penguin继承Bird,虽然企鹅是鸟,但企鹅不会飞,如果你在Bird里定义了fly()方法,子类企鹅就必须重写或抛出异常,这就破坏了 LSP(里氏替换原则),此时继承并非好选择。
子类与父类有大量共同实现,且未来不会发生剧烈变化
- 例如一个基础的数据访问层
BaseRepository提供了通用的find(),create(),update()方法。 UserRepository继承它,并添加专属于用户的findByEmail()。
// 适合继承的场景:模板方法模式
abstract class ReportGenerator {
final public function generate(): string {
$data = $this->fetchData();
$formatted = $this->formatData($data);
return $this->output($formatted);
}
abstract protected function fetchData(): array;
abstract protected function formatData(array $data): string;
protected function output(string $content): string {
return $content; // 默认实现
}
}
class PdfReport extends ReportGenerator {
protected function fetchData(): array { /* ... */ }
protected function formatData(array $data): string { /* ... */ }
protected function output(string $content): string {
return "PDF: " . $content; // 重写输出方式
}
}
框架或语言强制要求
- 在 Laravel 中,控制器继承
Controller基类。 - 视图组件继承
Component类。 - 这些场景下,继承是框架设计的一部分,用于提供基础设施。
什么时候用 组合?
组合将功能委托给独立的对象,通过“拥有”来复用,当你发现继承带来的问题(如深层继承链、脆弱的基类、不必要的接口污染)时,组合是更好的选择。
行为需要在不同类之间共享,但它们没有 is-a 关系
Order,Invoice,Customer都需要日志和发送通知的能力。- 用继承?让它们都继承
Notifiable?这会让类层次混乱(一个客户“是”一个通知发送者吗?)。 - 用组合?让它们都“拥有”一个
LoggerInterface和NotifierInterface实例。
// 组合:通过构造函数注入行为
class OrderService {
public function __construct(
private LoggerInterface $logger,
private NotifierInterface $notifier
) {}
public function process(Order $order): void {
try {
// 处理订单...
$this->notifier->send(new OrderConfirmation($order));
} catch (\Exception $e) {
$this->logger->error('Order processing failed', ['exception' => $e]);
throw $e;
}
}
}
需要在运行时动态改变行为
- 继承是静态的,子类在编译时就固定了行为。
- 组合可以在运行时通过 setter 或构造函数替换“策略”对象。
// 策略模式:组合的经典应用
interface PaymentStrategy {
public function pay(float $amount): void;
}
class CreditCardPayment implements PaymentStrategy { /* ... */ }
class PayPalPayment implements PaymentStrategy { /* ... */ }
class Checkout {
public function __construct(private PaymentStrategy $paymentStrategy) {}
// 运行时切换支付方式
public function setPaymentStrategy(PaymentStrategy $strategy): void {
$this->paymentStrategy = $strategy;
}
public function process(float $amount): void {
$this->paymentStrategy->pay($amount);
}
}
避免“钻石问题”和深层继承链
- PHP 不支持多继承,但通过组合可以模拟多继承。
- 如果你的类需要同时拥有日志、缓存、事件调度等功能,组合可以避免创建
LoggedCachedEventDispatcher这样的上帝类。
// 组合模拟多继承
class UserService {
private Logger $logger;
private CacheInterface $cache;
public function __construct(Logger $logger, CacheInterface $cache) {
$this->logger = $logger;
$this->cache = $cache;
}
public function getUser(int $id): ?User {
$cacheKey = "user_$id";
return $this->cache->remember($cacheKey, 3600, function() use ($id) {
$this->logger->info("Fetching user $id from DB");
// 从数据库获取...
});
}
}
当继承会导致“脆弱的基类”问题时
- 对父类的任何修改(如添加一个私有或受保护方法)都可能影响所有子类。
- 组合通过接口契约进行交互,底层改动通常限缩在具体实现类内部。
决策流程图
你可以用下面的思维模型快速判断:
-
这个关系真的是
is-a吗?还是只是方便复用代码?- 是
is-a→ 跳到第 2 步。 - 只是为了复用代码 → 用组合。
- 是
-
子类是否会完全替换父类的某些方法(而不是扩展)?
- 会(比如重写方法并留空或抛出异常)→ 用组合(或者接口)。
- 不会,只是在父类基础上增加逻辑 → 继续看第 3 步。
-
未来是否会有其他不相关的子类加入这个继承体系?
- 会(比如有
Car继承Vehicle,未来还有Boat,Plane)→ 用组合(通过策略或接口)。 - 层次结构稳定,变动极小 → 可以谨慎使用继承(如模板方法)。
- 会(比如有
-
你是否正在使用框架,且框架要求继承?
- 是 → 用继承(遵循框架约定)。
- 否 → 回到第 1 步,优先考虑组合。
一个具体的例子:通知系统
假设你要实现一个能发送多种通知的系统(邮件、短信、App推送)。
用继承的错误做法:
class Notification {
public function send(string $message): void { /* 空实现 */ }
}
class EmailNotification extends Notification { /* 重写 send */ }
class SmsNotification extends Notification { /* 重写 send */ }
// 问题:如果我想发送同时包含邮件和短信的通知?
// 要么再建一个 MultipleNotification 继承,要么组合多个子类,但这不是继承的合理场景。
用组合的正确做法:
interface NotifierInterface {
public function send(string $message): void;
}
class EmailNotifier implements NotifierInterface { /* ... */ }
class SmsNotifier implements NotifierInterface { /* ... */ }
class PushNotifier implements NotifierInterface { /* ... */ }
// 组合实现复合通知
class MultiChannelNotifier implements NotifierInterface {
private array $notifiers;
public function __construct(NotifierInterface ...$notifiers) {
$this->notifiers = $notifiers;
}
public function send(string $message): void {
foreach ($this->notifiers as $notifier) {
$notifier->send($message);
}
}
}
-
优先选择组合:
- 当需要低耦合、高灵活性、运行时行为可变的场景。
- 当复用的是行为而非结构时。
- 当
is-a关系存在疑问时。
-
保留继承:
- 当有明确的层次结构(
is-a),且子类不会破坏父类契约(LSP)。 - 当父类提供的是模板方法、基础设施、框架钩子时。
- 在不影响可维护性的前提下,用于减少样板代码。
- 当有明确的层次结构(
在 PHP 项目中,“接口 + 组合” 是比“继承”更加稳健的设计模式,组合让代码更易于测试(可以轻松替换依赖的模拟对象)、更易于扩展(新增行为只需新增实现类,无需修改现有类),而继承更适合那些稳定、成熟的层级结构。