最近我一直在努力学习PHP,我发现自己被trait缠住了。我理解横向代码重用的概念,并且不希望必然地继承抽象类。我不明白的是:使用特征和使用界面之间的关键区别是什么?

我曾试着搜索过一篇像样的博客文章或文章,解释什么时候使用其中一种或另一种,但到目前为止,我找到的例子似乎非常相似,甚至完全相同。


当前回答

主要的区别在于,对于接口,您必须在实现上述接口的每个类中定义每个方法的实际实现,因此您可以让许多类实现相同的接口但具有不同的行为,而trait只是注入到类中的代码块;另一个重要的区别是trait方法只能是类方法或静态方法,不像接口方法也可以(通常是)是实例方法。

其他回答

基本上,您可以将trait视为代码的自动“复制-粘贴”。

使用trait是很危险的,因为在执行前我们无法知道它的作用。

然而,由于缺乏遗传等限制,性状更加灵活。

trait在注入方法时很有用,它可以检查类中是否存在另一个方法或属性。这是一篇不错的文章(但是是法语,抱歉)。

对于能读法语的人来说,GNU/Linux杂志HS 54有一篇关于这个主题的文章。

描述trait的一个常用比喻是:trait是带有实现的接口。

在大多数情况下,这是一种很好的思考方式,但两者之间存在一些微妙的差异。

首先,instanceof操作符不能与trait一起工作(即,trait不是一个真正的对象),因此不能使用它来查看一个类是否具有某个trait(或者查看两个本来不相关的类是否共享一个trait)。这就是他们所说的横向代码重用的结构。

现在PHP中有一些函数可以让您获得类使用的所有特征的列表,但是特征继承意味着您需要进行递归检查,以可靠地检查类在某个时刻是否具有特定的特征(PHP doco页面上有示例代码)。不过,它当然不像instanceof那样简单干净,以我之见,这个特性会让PHP变得更好。

而且,抽象类仍然是类,因此它们不能解决与多重继承相关的代码重用问题。请记住,您只能扩展一个类(真实的或抽象的),但可以实现多个接口。

我发现trait和接口非常适合一起使用来创建伪多重继承。例如:

class SlidingDoor extends Door implements IKeyed  
{  
    use KeyedTrait;  
    [...] // Generally not a lot else goes here since it's all in the trait  
}

这样做意味着您可以使用instanceof来确定特定的Door对象是否是Keyed的,您知道您将得到一组一致的方法,等等,并且所有的代码在所有使用KeyedTrait的类中都位于一个位置。

其他答案很好地解释了界面和特征之间的差异。我将重点介绍一个有用的真实例子,特别是一个演示trait可以使用实例变量的例子——允许您用最少的样板代码向类添加行为。

再一次,像其他人提到的那样,特征与接口很好地配对,允许接口指定行为契约,而特征则完成实现。

在一些代码库中,向类添加事件发布/订阅功能是常见的场景。有3种常见的解决方案:

定义带有事件发布/订阅代码的基类,然后希望提供事件的类可以扩展它以获得功能。 定义一个带有事件发布/订阅代码的类,然后其他想要提供事件的类可以通过组合来使用它,定义自己的方法来包装组合对象,将方法调用代理给它。 用事件发布/订阅代码定义trait,然后其他想要提供事件的类可以使用该trait(也就是导入它)来获得功能。

它们的工作效果如何?

第一条效果不好。它会,直到有一天你意识到你不能扩展基类,因为你已经扩展了其他的东西。我将不展示这方面的示例,因为这样使用继承的局限性应该是显而易见的。

第二条和第三条都很有效。我将展示一个突出一些差异的例子。

首先,两个示例之间的一些代码是相同的:

一个接口

interface Observable {
    function addEventListener($eventName, callable $listener);
    function removeEventListener($eventName, callable $listener);
    function removeAllEventListeners($eventName);
}

以及一些演示用法的代码:

$auction = new Auction();

// Add a listener, so we know when we get a bid.
$auction->addEventListener('bid', function($bidderName, $bidAmount){
    echo "Got a bid of $bidAmount from $bidderName\n";
});

// Mock some bids.
foreach (['Moe', 'Curly', 'Larry'] as $name) {
    $auction->addBid($name, rand());
}

好了,现在让我们来看看在使用trait时Auction类的实现是如何不同的。

首先,这是#2(使用合成)的样子:

class EventEmitter {
    private $eventListenersByName = [];

    function addEventListener($eventName, callable $listener) {
        $this->eventListenersByName[$eventName][] = $listener;
    }

    function removeEventListener($eventName, callable $listener) {
        $this->eventListenersByName[$eventName] = array_filter($this->eventListenersByName[$eventName], function($existingListener) use ($listener) {
            return $existingListener === $listener;
        });
    }

    function removeAllEventListeners($eventName) {
        $this->eventListenersByName[$eventName] = [];
    }

    function triggerEvent($eventName, array $eventArgs) {
        foreach ($this->eventListenersByName[$eventName] as $listener) {
            call_user_func_array($listener, $eventArgs);
        }
    }
}

class Auction implements Observable {
    private $eventEmitter;

    public function __construct() {
        $this->eventEmitter = new EventEmitter();
    }

    function addBid($bidderName, $bidAmount) {
        $this->eventEmitter->triggerEvent('bid', [$bidderName, $bidAmount]);
    }

    function addEventListener($eventName, callable $listener) {
        $this->eventEmitter->addEventListener($eventName, $listener);
    }

    function removeEventListener($eventName, callable $listener) {
        $this->eventEmitter->removeEventListener($eventName, $listener);
    }

    function removeAllEventListeners($eventName) {
        $this->eventEmitter->removeAllEventListeners($eventName);
    }
}

下面是第三点(特质):

trait EventEmitterTrait {
    private $eventListenersByName = [];

    function addEventListener($eventName, callable $listener) {
        $this->eventListenersByName[$eventName][] = $listener;
    }

    function removeEventListener($eventName, callable $listener) {
        $this->eventListenersByName[$eventName] = array_filter($this->eventListenersByName[$eventName], function($existingListener) use ($listener) {
            return $existingListener === $listener;
        });
    }

    function removeAllEventListeners($eventName) {
        $this->eventListenersByName[$eventName] = [];
    }

    protected function triggerEvent($eventName, array $eventArgs) {
        foreach ($this->eventListenersByName[$eventName] as $listener) {
            call_user_func_array($listener, $eventArgs);
        }
    }
}

class Auction implements Observable {
    use EventEmitterTrait;

    function addBid($bidderName, $bidAmount) {
        $this->triggerEvent('bid', [$bidderName, $bidAmount]);
    }
}

注意,EventEmitterTrait内部的代码与EventEmitter类内部的代码完全相同,只是trait将triggerEvent()方法声明为受保护。因此,您需要注意的唯一区别是Auction类的实现。

And the difference is large. When using composition, we get a great solution, allowing us to reuse our EventEmitter by as many classes as we like. But, the main drawback is the we have a lot of boilerplate code that we need to write and maintain because for each method defined in the Observable interface, we need to implement it and write boring boilerplate code that just forwards the arguments onto the corresponding method in our composed the EventEmitter object. Using the trait in this example lets us avoid that, helping us reduce boilerplate code and improve maintainability.

然而,有时你可能不希望你的Auction类实现完整的Observable接口——也许你只想公开1到2个方法,甚至可能根本不公开,这样你就可以定义自己的方法签名。在这种情况下,您可能仍然喜欢组合方法。

但是,在大多数情况下,这个特性非常引人注目,特别是当接口有很多方法时,这会导致您编写大量的样板文件。

*你实际上可以两者都做——定义EventEmitter类,以防你想组合使用它,并定义EventEmitterTrait trait,使用EventEmitter类实现在trait里面:)

主要的区别在于,对于接口,您必须在实现上述接口的每个类中定义每个方法的实际实现,因此您可以让许多类实现相同的接口但具有不同的行为,而trait只是注入到类中的代码块;另一个重要的区别是trait方法只能是类方法或静态方法,不像接口方法也可以(通常是)是实例方法。

公共服务公告:

我想要声明的是,我相信trait几乎总是一种代码气味,应该避免使用组合。在我看来,单继承经常被滥用到反模式的地步,而多重继承只会加剧这个问题。在大多数情况下,使用组合而不是继承(无论是单个还是多个)会更好。如果您仍然对特征及其与界面的关系感兴趣,请继续阅读…


让我们这样开始:

面向对象编程(OOP)可能是一个难以掌握的范例。 仅仅因为你在使用类并不意味着你的代码就是 面向对象(OO)。

要编写OO代码,你需要理解OOP实际上是关于对象的功能。你必须考虑类可以做什么,而不是它们实际做什么。这与传统的过程式编程形成了鲜明的对比,在传统的过程式编程中,重点是让一小段代码“做一些事情”。

如果面向对象编程是关于规划和设计的,那么接口就是蓝图,对象就是完全建成的房子。与此同时,特征只是一种帮助建造蓝图(界面)所布置的房子的方法。

接口

那么,我们为什么要使用接口呢?很简单,接口使我们的代码不那么脆弱。如果您怀疑这种说法,可以问问那些被迫维护遗留代码的人,这些代码不是针对接口编写的。

接口是程序员和他/她的代码之间的契约。这个界面说:“只要你遵守我的规则,你可以随心所欲地实现我,我保证不会破坏你的其他代码。”

举个例子,考虑一个现实世界的场景(没有汽车或小部件):

你想为一个web应用程序实现一个缓存系统 服务器负载下降

你开始写一个类来缓存请求响应使用APC:

class ApcCacher
{
  public function fetch($key) {
    return apc_fetch($key);
  }
  public function store($key, $data) {
    return apc_store($key, $data);
  }
  public function delete($key) {
    return apc_delete($key);
  }
}

然后,在HTTP响应对象中,在执行生成实际响应的所有工作之前检查缓存命中:

class Controller
{
  protected $req;
  protected $resp;
  protected $cacher;

  public function __construct(Request $req, Response $resp, ApcCacher $cacher=NULL) {
    $this->req    = $req;
    $this->resp   = $resp;
    $this->cacher = $cacher;

    $this->buildResponse();
  }

  public function buildResponse() {
    if (NULL !== $this->cacher && $response = $this->cacher->fetch($this->req->uri()) {
      $this->resp = $response;
    } else {
      // Build the response manually
    }
  }

  public function getResponse() {
    return $this->resp;
  }
}

这种方法非常有效。但也许几周后你决定使用基于文件的缓存系统而不是APC。现在你必须改变你的控制器代码,因为你已经把你的控制器编程为使用ApcCacher类的功能,而不是一个表达ApcCacher类功能的接口。让我们假设你让Controller类依赖于CacherInterface而不是具体的ApcCacher,就像这样:

// Your controller's constructor using the interface as a dependency
public function __construct(Request $req, Response $resp, CacherInterface $cacher=NULL)

你可以这样定义你的界面:

interface CacherInterface
{
  public function fetch($key);
  public function store($key, $data);
  public function delete($key);
}

反过来,你让你的ApcCacher和你的新FileCacher类实现CacherInterface,你编程你的Controller类来使用接口所需的功能。

这个示例(希望)演示了如何通过接口编程来更改类的内部实现,而不用担心这些更改是否会破坏其他代码。

特征

另一方面,trait只是一种重用代码的方法。界面不应该被认为是与特征相排斥的选择。事实上,创建满足接口所需功能的特征是最理想的用例。

只有当多个类共享相同的功能(可能由相同的接口决定)时,才应该使用trait。使用trait为单个类提供功能是没有意义的:这只会混淆类的功能,更好的设计应该将trait的功能移到相关的类中。

考虑下面的trait实现:

interface Person
{
    public function greet();
    public function eat($food);
}

trait EatingTrait
{
    public function eat($food)
    {
        $this->putInMouth($food);
    }

    private function putInMouth($food)
    {
        // Digest delicious food
    }
}

class NicePerson implements Person
{
    use EatingTrait;

    public function greet()
    {
        echo 'Good day, good sir!';
    }
}

class MeanPerson implements Person
{
    use EatingTrait;

    public function greet()
    {
        echo 'Your mother was a hamster!';
    }
}

一个更具体的例子:假设接口讨论中的FileCacher和ApcCacher都使用相同的方法来确定缓存项是否过时,是否应该删除(显然在现实生活中不是这样的,但还是这样吧)。您可以编写一个trait,并允许两个类使用它来满足公共接口需求。

最后提醒一句:注意不要在性格特征上走极端。当独特的类实现就足够了的时候,特征常常被用作糟糕设计的拐杖。为了获得最佳的代码设计,你应该限制特征以满足界面需求。