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

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


当前回答

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

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

在一些代码库中,向类添加事件发布/订阅功能是常见的场景。有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则赋予对象做这件事的能力。

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

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

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

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

描述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的类中都位于一个位置。