PHP项目继承与组合取舍

wen PHP项目 2

本文目录导读:

PHP项目继承与组合取舍

  1. 核心概念速览
  2. 什么时候用 继承
  3. 什么时候用 组合
  4. 决策流程图
  5. 一个具体的例子:通知系统

在 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?这会让类层次混乱(一个客户“是”一个通知发送者吗?)。
  • 用组合?让它们都“拥有”一个 LoggerInterfaceNotifierInterface 实例。
// 组合:通过构造函数注入行为
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");
            // 从数据库获取...
        });
    }
}

当继承会导致“脆弱的基类”问题时

  • 对父类的任何修改(如添加一个私有或受保护方法)都可能影响所有子类。
  • 组合通过接口契约进行交互,底层改动通常限缩在具体实现类内部。

决策流程图

你可以用下面的思维模型快速判断:

  1. 这个关系真的是 is-a 吗?还是只是方便复用代码?

    • is-a → 跳到第 2 步。
    • 只是为了复用代码 → 用组合
  2. 子类是否会完全替换父类的某些方法(而不是扩展)?

    • 会(比如重写方法并留空或抛出异常)→ 用组合(或者接口)。
    • 不会,只是在父类基础上增加逻辑 → 继续看第 3 步。
  3. 未来是否会有其他不相关的子类加入这个继承体系?

    • 会(比如有 Car 继承 Vehicle,未来还有 BoatPlane)→ 用组合(通过策略或接口)。
    • 层次结构稳定,变动极小 → 可以谨慎使用继承(如模板方法)。
  4. 你是否正在使用框架,且框架要求继承?

    • 是 → 用继承(遵循框架约定)。
    • 否 → 回到第 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 项目中,“接口 + 组合” 是比“继承”更加稳健的设计模式,组合让代码更易于测试(可以轻松替换依赖的模拟对象)、更易于扩展(新增行为只需新增实现类,无需修改现有类),而继承更适合那些稳定、成熟的层级结构。

抱歉,评论功能暂时关闭!