最近我一直在努力学习PHP,我发现自己被trait缠住了。我理解横向代码重用的概念,并且不希望必然地继承抽象类。我不明白的是:使用特征和使用界面之间的关键区别是什么?
我曾试着搜索过一篇像样的博客文章或文章,解释什么时候使用其中一种或另一种,但到目前为止,我找到的例子似乎非常相似,甚至完全相同。
最近我一直在努力学习PHP,我发现自己被trait缠住了。我理解横向代码重用的概念,并且不希望必然地继承抽象类。我不明白的是:使用特征和使用界面之间的关键区别是什么?
我曾试着搜索过一篇像样的博客文章或文章,解释什么时候使用其中一种或另一种,但到目前为止,我找到的例子似乎非常相似,甚至完全相同。
当前回答
该特征与我们可以用于多重继承目的和代码可重用性的类相同。
我们可以在类中使用trait,也可以在同一个类中使用'use keyword'来使用多个trait。
接口用于代码可重用性与特性相同
接口是扩展多个接口,所以我们可以解决多个继承问题,但当我们实现接口时,我们应该在类中创建所有的方法。 欲了解更多信息,请点击以下链接:
http://php.net/manual/en/language.oop5.traits.php http://php.net/manual/en/language.oop5.interfaces.php
其他回答
trait只是为了代码重用。
Interface只是提供了要在类中定义的函数的签名,它可以根据程序员的判断使用。这样就为我们提供了一组类的原型。
供参考, http://www.php.net/manual/en/language.oop5.traits.php
主要的区别在于,对于接口,您必须在实现上述接口的每个类中定义每个方法的实际实现,因此您可以让许多类实现相同的接口但具有不同的行为,而trait只是注入到类中的代码块;另一个重要的区别是trait方法只能是类方法或静态方法,不像接口方法也可以(通常是)是实例方法。
基本上,您可以将trait视为代码的自动“复制-粘贴”。
使用trait是很危险的,因为在执行前我们无法知道它的作用。
然而,由于缺乏遗传等限制,性状更加灵活。
trait在注入方法时很有用,它可以检查类中是否存在另一个方法或属性。这是一篇不错的文章(但是是法语,抱歉)。
对于能读法语的人来说,GNU/Linux杂志HS 54有一篇关于这个主题的文章。
该特征与我们可以用于多重继承目的和代码可重用性的类相同。
我们可以在类中使用trait,也可以在同一个类中使用'use keyword'来使用多个trait。
接口用于代码可重用性与特性相同
接口是扩展多个接口,所以我们可以解决多个继承问题,但当我们实现接口时,我们应该在类中创建所有的方法。 欲了解更多信息,请点击以下链接:
http://php.net/manual/en/language.oop5.traits.php http://php.net/manual/en/language.oop5.interfaces.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里面:)