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

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


当前回答

接口是一种契约,它表明“这个对象能够做这件事”,而trait则赋予对象做这件事的能力。

trait本质上是在类之间“复制和粘贴”代码的一种方式。

试着阅读这篇文章,PHP的特点是什么?

其他回答

接口是一种契约,它表明“这个对象能够做这件事”,而trait则赋予对象做这件事的能力。

trait本质上是在类之间“复制和粘贴”代码的一种方式。

试着阅读这篇文章,PHP的特点是什么?

其他答案很好地解释了界面和特征之间的差异。我将重点介绍一个有用的真实例子,特别是一个演示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只是为了代码重用。

Interface只是提供了要在类中定义的函数的签名,它可以根据程序员的判断使用。这样就为我们提供了一组类的原型。

供参考, http://www.php.net/manual/en/language.oop5.traits.php

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

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

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

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

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

如果你懂英语,知道trait的意思,它就是这个名字的意思。它是一个无类的方法和属性包,您可以通过输入use将其附加到现有类。

基本上,你可以将它与单个变量进行比较。闭包函数可以从作用域外部使用这些变量,这样它们就有了作用域内部的值。他们是强大的,可以在任何地方使用。如果trait被使用,也会发生同样的情况。