使用trait的实例介绍

47 阅读4分钟

什么时候使用特质?从来没有。

好吧,一个特性可以被认为有一些好处。

优点

  1. 如果你想在多个类之间重复使用一些代码,使用trait是扩展类的一种选择。在这种情况下,trait可能是更好的选择,因为它不会成为类型层次结构的一部分,也就是说,使用trait的类并不是 "该trait的实例"。
  2. 特质可以通过提供编译时的复制/粘贴来节省一些手动复制/粘贴的工作。

缺点

另一方面,特质也有几个问题。比如说:

  • 当一个trait给一个类增加了一个或多个公共方法时,你经常会遇到需要将这些方法定义为一个接口,而该类将通过使用trait自动实现这些方法。这是不可能的。特质不能实现一个接口。因此,如果你想实现这一点,你必须使用trait明确地实现接口。
  • 特质没有自己的 "私有 "方法和属性。我经常喜欢从使用trait的类中隐藏一些东西,但不可能把东西变成 "trait-private"。任何在trait中被定义为私有的东西仍然会被使用trait的类所访问。这使得trait不可能对任何东西进行封装

更好的选择

总之,在实践中,我总是能找到更好的方法来使用trait。举例来说,这里有一些替代方案。

  • 如果trait有服务职责,最好把它变成一个实际的服务,它可以而且应该作为构造参数注入需要这个服务的服务中。这就是所谓的组合行为,而不是继承行为。当你想摆脱父类时也可以使用这种方法。
  • 如果特质给一个实体增加了一些行为,而这些行为对另一个实体来说是相同的,那么引入一个值对象并使用它往往是有意义的。
  • 当你在模型的不同部分有相同的行为时,另一个选择是直接复制代码。这将使每个对象的设计保持足够的灵活性,因此每个对象都可以朝着自己的方向发展。当我们想改变逻辑时,我们就不必担心其他地方会重复使用同样的逻辑。

一个反例

尽管大多数情况下并不真正需要特质,而且有更好的替代品,但有一种情况我一直在使用特质。这是该特性的代码,如果你参加过我的研讨会,你会知道它。

trait EventRecording
{
    /**
     * @var list<object>
     */
    private array $events = [];

    private function recordThat(object $event): void
    {
        $this->events[] = $event;
    }

    /**
     * @return list<object>
     */
    public function releaseEvents(): array
    {
        $events = $this->events;

        $this->events = [];

        return $events;
    }
}

我在实体中使用这个trait,因为在那里我总是想做同样的事情:记录任何数量的事件,然后在保存实体的修改状态后,释放这些事件,以便分派它们。

一个担心可能是,releaseEvents() 是一个公共方法。但它不一定要在任何接口上。我们不需要一个Entity 接口,也不需要一个RecordsEvents 接口,除非我们想创建一些可重复使用的代码,可以保存一个实体并调度其记录的事件。

另一个值得关注的问题是,这个trait缺少 "trait-private"。举例来说,使用这个特质的实体也可以不使用$this->recordThat(...) ,而只做$this->events[] = ...

我们可以通过将代码提取到一个对象中来解决这个问题,(为 "使用特质总是有替代方案 "的说法添加更多证据)。

final class EventRecording
{
    /**
     * @var list<object>
     */
    private array $events = [];

    public function recordThat(object $event): void
    {
        // ...
    }

    /**
     * @return list<object>
     */
    public function releaseEvents(): array
    {
        // ...
    }
}

然后我们需要把这个新类的一个实例分配给实体的一个私有属性。

final class SomeEntity
{
    // Can we do this already? I forgot
    private EventRecording $eventRecording = new EventRecording();

    /**
     * @return list<object>
     */
    public function releaseEvents(): array
    {
        return $this->eventRecording->releaseEvents();
    } 

    // ...
}

每个实体仍然需要那个公共方法releaseEvents() ,以及那个额外的私有属性,我们将其复制/粘贴到每个新实体类中。我们可以再次为此引入一个特质。

trait ReleaseEvents
{
    private EventRecording $eventRecording = new EventRecording();

    /**
     * @return list<object>
     */
    public function releaseEvents(): array
    {
        return $this->eventRecording->releaseEvents();
    } 
}

在这一点上,我觉得提取的EventRecording 类并没有做太多的事情,而且可能被认为是一个懒惰的类(代码气味)。我们不妨使用原来的trait,接受设计上的顾虑,然后就可以了。好吧。

对特质的要求

根据我对traits的经验(在项目、框架和库中看到过它们的例子),我不知道有什么好的案例可以使用traits,所以我的原则是永远不使用它们。但是,你可能有一些很好的特质例子,这些特质是有意义的,而且只有作为特质才有意义。因此,这里是我对特征的要求。请通过发表评论来分享你的例子。