为什么我避免使用 PHP Traits(以及我改用什么)

发布: (2026年2月2日 GMT+8 21:21)
5 min read
原文: Dev.to

Source: Dev.to

抱歉,我无法直接访问外部链接获取文章内容。请您把需要翻译的文本粘贴在这里,我会按照要求将其翻译成简体中文并保留原有的格式。

什么是特质

特质出现是为了解决单继承的局限性而做出的折衷方案。它们既不是继承,也不是组合,更不是接口。简而言之,特质是一种在类中“粘合”代码而无需显式依赖的方式。在底层,它的工作方式类似于简单的复制‑粘贴。

为什么它们会导致问题

  • 可读性差 – 当我看到一个类使用 use SomeTrait 时,我不知道该类到底做了什么。我必须打开 trait,检查它期望的受保护方法和属性,并查看它是否覆盖了某些东西。行为变得不明显。
  • 隐藏耦合 – Trait 常常使用类的受保护属性或调用类本身未实现的受保护方法。这会产生双向、隐式的耦合:trait 知道类的内部细节,而类依赖于 trait 的内部实现。构造函数或方法签名中看不出这种关系。
  • 封装破坏 – Trait 鼓励访问类的内部状态。没有明确的契约和显式的依赖,而是出现“trait 期待某处有一个 $service”。修改内部结构会导致 trait 失效。
  • 难以测试 – 不能直接实例化 trait。要测试它,需要创建一个伪类,设置所有受保护的依赖,并且希望没有遗漏。这并不是单元测试,而是变通办法。
  • 架构混乱 – PHP 允许在 trait 中使用 trait,且一个类可以使用多个 trait。这很容易导致菱形继承问题、纠结的依赖链,以及“能跑”但没人懂其工作原理的代码。

关于 PHP 8.x 改进?

PHP 8 添加了抽象方法、常量以及对 trait 中静态属性的更改。不幸的是,从 SOLID 和 clean‑architecture 的角度来看,这让情况更糟——trait 现在更深入地涉及继承思考和静态状态。

典型的异味

如果一个 trait 拥有受保护的方法并且使用了类的受保护服务,这几乎总是意味着缺少一个应该被提取为独立类的对象。

我通常使用的替代方案

  • Dependency Injection – 依赖是显式的;代码可从构造函数中阅读。
  • Composition – 使用 “has‑a” 而不是 “uses”。
  • Strategy / Factory – 尤其是代替受保护的辅助方法。
  • A simple class or function – 如果 trait 没有状态,可能不需要。

所有这些都使依赖可见,提升可测试性,并保持架构简洁。

快速重构示例

之前(使用 trait)

trait NotifiableTrait
{
    protected function notifyUser(string $message)
    {
        $this->notificationService->send($this->user, $message);
    }
}

final class OrderCreateAction
{
    use NotifiableTrait;

    public function handle(OrderCreateDTO $dto)
    {
        // ...order logic...

        $this->notifyUser('Order created!'); // where is this from?
    }
}

之后(使用依赖注入)

final class OrderCreateAction
{
    public function __construct(private readonly UserNotifier $notifier) {}

    public function handle(OrderCreateDTO $dto)
    {
        // ...order logic...

        $this->notifier->send($user, 'Order created!'); // explicit and clear
    }
}

现在依赖关系是可见的、可测试的且明确的。

结论

特性是一种捷径,往往会导致后期进行大量重构。如果行为可以提取到单独的对象中,应该进行提取。如果特性没有状态,可能根本不需要存在。我并不禁止使用特性;我只是尽量避免以后为它们付出代价。

参考文献

Back to Blog

相关文章

阅读更多 »