使用特征为接口定义函数的目的是什么

What is the purpose of using traits to define functions for an interface

抱歉,如果这是一个重复的问题或一个共同的设计原则,我已经四处搜索但无法找到这个问题的任何答案。我可能只是在搜索错误的关键字。

我一直在查看一个流行的库 Sabre/Event (https://sabre.io/event/),代码中有一个我试图理解的简单 class/inheritance 模型:

classEventEmitter 实现 EventEmitterInterface 并使用 EventEmitterTrait(代码见下文)。

在 class 上方的 EventEmitterTrait 中有一条评论说:

* Using the trait + interface allows you to add EventEmitter capabilities
* without having to change your base-class.

我试图理解为什么这条评论这么说,为什么它允许在不改变基础的情况下添加功能 class,以及这与将例程放入 EventEmitter 本身有何不同。

您不能只扩展 EventEmitter 并在派生的 class 中添加功能吗?

简化代码:

// EventEmitter.php
class EventEmitter implements EventEmitterInterface {
    use EventEmitterTrait;
}


// EventEmitterInterface.php
interface EventEmitterInterface {
    // ... declares several function prototypes
}

// EventEmitterTrait.php
trait EventEmitterTrait {
    // ... implements the routines declared in EventEmitterInterface
}

Per Sabre 在 "Integration into other objects" 上的 Event Emitter's docs:

To add Emitter capabilities to any class, you can simply extend it.

If you cannot extend, because the class is already part of an existing class hierarchy you can use the supplied trait.

所以在这种情况下,想法是如果您使用自己的对象,这些对象已经是 class 层次结构的一部分,您可以简单地实现接口 + 使用特征,而不是扩展发射器class(你做不到)。

Integration into other objects 文档说:

If you cannot extend, because the class is already part of an existing class hierarchy you can use the supplied trait".

我知道这是一种变通方法,当您已经有了不想更改的 OOP 设计并且想要添加事件功能时。例如:

Model -> AppModel -> Customer

PHP 没有多重继承,因此 Customer 可以扩展 AppModelEmitter 但不能同时扩展两者。如果您在 Customer 中实现接口,则代码在其他地方不可重用;如果您实施例如AppModel随处可用,这可能并不理想。

有了 traits,您可以编写自定义事件代码并在重用它的地方挑选。

你基本上是在问两个问题。

  1. 什么是接口,它们为什么有用?
  2. 什么是特质,它们为什么有用?

要理解为什么接口有用,您必须对继承和一般的 OOP 有所了解。如果您以前听说过术语 spaghetti code(当您倾向于编写如此纠结在一起的命令式代码时,您几乎无法理解它)那么您应该将其比作术语OOP 的千层面代码(当你将 class 扩展到如此多的层时,很难理解哪一层在做什么)。

1。接口

接口通过允许 class 实现一组通用方法而不必限制 class 的层次结构来消除一些混乱。我们不从基础 class 派生接口。我们只是 将它们实现 到给定的 class.

PHP 中的一个非常明显的例子是 DateTimeInterface。它提供了一组 DateTimeDateTimeImmutable 都将实现的通用方法。它 而不是 ,但是,它告诉那些 class 实施是什么。 class 是一个实现。接口只是 class 无实现的方法。但是,由于两者都实现相同的接口,因此很容易测试 任何实现该接口 的 class,因为您知道它们将 总是 有相同的方法。所以我知道 DateTimeDateTimeImmutable 都将实现方法 format,它将接受 String 作为输入,return 接受 String,不管哪个 class 正在实施它。我什至可以编写自己的 DateTime 实现来实现 DateTimeInterface,并且保证该方法具有相同的签名。

所以假设我写了一个接受 DateTime 对象的方法,并且该方法期望 运行 该对象上的 format 方法。如果它不关心给它哪个 class,具体来说,那么该方法可以简单地将其原型类型提示为 DateTimeInterface。现在任何人都可以在他们自己的 class 中自由实现 DateTimeInterface,而不必从某些基础 class 扩展,并为我的方法提供 保证 [=88] 的对象=] 以同样的方式工作。


因此,对于您的 EventEmitter 示例,您可以将 class(如 DateTime)的相同功能添加到 any class 甚至可能不会从 DateTime 扩展,但只要我们知道它实现了相同的接口,我们就可以肯定地知道它具有相同的方法和相同的签名。这对 EventEmitter.

意味着同样的事情

2。特质

Traits,与接口不同,实际上可以提供实现。它们也是水平继承的一种形式,不同于扩展 classes 的垂直继承。因为两个完全不同的class不是从同一个基数class派生出来的,可以使用同一个Trait。这是可能的,因为在 PHP 中,特征基本上只是编译器辅助的复制和粘贴。想象一下,您从字面上复制了特征内部的代码,然后将其粘贴到每个 class 中,并在编译时之前使用它。你会得到相同的结果。您只是将代码注入不相关的 classes.

这很有用,因为有时您有一个方法或一组方法证明可以在两个不同的 class 中重用,即使其余 class 个没有其他共同点。

例如,假设您正在编写一个 CMS,其中有一个 Document class 和一个 User class。这两个 class 都没有任何有意义的关联。他们做的事情截然不同,其中一个扩展另一个是没有意义的。但是,它们都有一个共同的特定行为:flag() 方法表明该对象已被用户标记为违反服务条款。

trait FlagContent {
    public function flag(Int $userId, String $reason): bool {
        $this->flagged = true;
        $this->byUserId = $userId;
        $this->flagReason = $reason;

        return $this->updateDatabase();
    }
}

现在考虑一下,也许您的 CMS 有其他内容可能会被标记,例如 Image class,或 Video class,甚至 Comment class。这些 class 通常都是不相关的。仅使用特定的 class 来标记内容可能没有多大意义,尤其是当相关对象的属性必须传递给此 class 以更新数据库时。从基数 class 派生它们也没有意义(它们彼此完全无关)。在每个 class 中重写相同的代码也没有意义,因为在一个地方而不是多个地方更改它会更容易。

所以这里似乎最明智的是使用 Trait.


再一次,关于您的 EventEmitter 示例,它们为您提供了一些可以在您的实施中重用的特征 class 基本上可以更轻松地重用代码而无需扩展从基础 class(水平继承)。

这是一个有趣的问题,我将尝试给出我的看法。正如你所问,

使用 traits 为接口定义函数的目的是什么?

Traits 基本上使您能够创建一些可重用的代码或功能,然后可以在您的代码库中的任何地方使用这些代码或功能。现在,PHP 不支持多重继承,因此特性和接口可以解决这个问题。这里的问题是为什么有特征??想象一下下面的场景,

class User
{
  public function hasRatings()
  {
    // some how we want users to have ratings
  }

  public function hasBeenFavorited()
  {
    // other users can follow
  }

  public function name(){}
  public function friends(){}

  // and a few other methods
}

现在假设我们有一个 post class,它与用户具有相同的逻辑,可以通过 hasRatings()hasBeenFavorited() 方法来实现。现在,一种方法是简单地从 User Class.

继承
class Post extends User
{
  // Now we have access to the mentioned methods but we have inherited
  // methods and properties which is not really needed here
}

因此,为了解决这个问题,我们可以使用特征。

trait UserActions 
   {
     public function hasRatings()
      {
        // some how we want users to have ratings
      }

      public function hasBeenFavorited()
      {
        // other users can follow
      }

   }

有了这些逻辑,我们现在可以在代码中任何需要它的地方使用它。

class User
{
  use UserActions;
}

class Post
{
  use UserActions;
}

现在假设我们有一个报告 class,我们希望在其中根据用户操作生成特定报告。

class Report 
{
   protected $user;

   public function __construct(User $user)
   {
     $this->user = $user
   }

   public function generate()
   {
     return $this->user->hasRatings();
   }
}

现在,如果我想为 Post 生成报告,会发生什么情况?实现这一目标的唯一方法是更新另一份报告 class 即可能 PostReport.. 你能看到我的意思吗?当然可以有另一种方式,我不必重复自己。这就是接口或契约应运而生的地方。牢记这一点,让我们重新定义我们的报告 class 并使其接受合同而不是具体的 class 这将始终确保我们可以访问 UserActions.

interface UserActionable
{
   public function hasRatings();

   public function hasBeenFavorited();
}



 class Report 
    {
       protected $actionable;

       public function __construct(UserActionable $actionable)
       {
         $this->actionable = $actionable;
       }

       public function generate()
       {
         return $this->actionable->hasRatings();
       }
    }

//lets make our post and user implement the contract so we can pass them
// to report

class User implements UserActionable
{
  uses UserActions;
}

class Post implements UserActionable
{
  uses UserActions;
}

// Great now we can switch between user and post during run time to generate 
// reports without changing the code base

$userReport = (new Report(new User))->generate();
$postReport = (new Report(new Post))->generate();

所以简而言之,接口和特征帮助我们实现基于 SOLID 原则的设计、更多的解耦代码和更好的组合。希望有帮助