Traits vs. Interfaces
最近我一直在努力学习PHP,我发现自己已经被特性挂掉了。我理解水平代码重用的概念,不希望从抽象类继承。我不明白的是,使用特性和界面之间的关键区别是什么?
我试着找一篇像样的博客文章或文章来解释什么时候使用一个或另一个,但到目前为止,我发现的例子似乎非常相似,以致于完全相同。
有人能分享他们对此的看法吗?
公共服务公告:好的。
我想声明的是,我相信特性几乎总是一种代码味道,应该避免有利于合成。我的观点是,单一继承经常被滥用到反模式的地步,多重继承只会加剧这个问题。在大多数情况下,通过支持组合而不是继承(无论是单继承还是多继承),您会得到更好的服务。如果你仍然对特性及其与界面的关系感兴趣,请继续阅读……好的。
我们先说:好的。
Object-Oriented Programming (OOP) can be a difficult paradigm to grasp.
Just because you're using classes doesn't mean your code is
Object-Oriented (OO).Ok.
要编写OO代码,您需要了解OOP实际上是关于对象的功能。你必须从课程能做什么而不是他们实际做什么的角度来考虑课程。这与传统的程序化编程形成了鲜明的对比,传统程序化编程的重点是让代码"做点什么"。好的。
如果OOP代码是关于规划和设计的,那么一个接口就是蓝图,一个对象就是完全构建的房子。同时,特征只是帮助建造蓝图(界面)布置的房子的一种方法。好的。界面
那么,我们为什么要使用接口呢?很简单,接口使我们的代码不那么脆弱。如果您怀疑这个声明,请询问任何被迫维护不是针对接口编写的遗留代码的人。好的。
接口是程序员和他/她的代码之间的契约。界面说,"只要你按照我的规则玩,你就可以随心所欲地实现我,我保证不会破坏你的其他代码。"好的。
因此,作为一个例子,考虑一个真实的场景(没有汽车或小部件):好的。
You want to implement a caching system for a web application to cut
down on server loadOk.
首先,使用apc将类写入缓存请求响应:好的。
1 2 3 4 5 6 7 8 9 10 11 12 | 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响应对象中,在执行所有生成实际响应的工作之前检查缓存命中:好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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。现在您必须更改控制器代码,因为您已将控制器编程为使用
1 2 | // your controller's constructor using the interface as a dependency public function __construct(Request $req, Response $resp, CacherInterface $cacher=NULL) |
要遵循这一点,您可以这样定义接口:好的。
1 2 3 4 5 6 | interface CacherInterface { public function fetch($key); public function store($key, $data); public function delete($key); } |
反过来,您的
这个例子(希望)演示了如何编程到一个接口,允许您更改类的内部实现,而不必担心更改是否会破坏其他代码。好的。特点
另一方面,特性只是一种重新使用代码的方法。接口不应该被认为是特性的互斥替代品。事实上,创建满足接口所需功能的特性是理想的用例。好的。
只有当多个类共享相同的功能(可能由同一个接口指定)时,才应该使用特性。使用一个特性为单个类提供功能是没有意义的:这只会混淆类的功能,更好的设计会将特性的功能转移到相关的类中。好的。
考虑以下特性实现:好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | 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!'; } } |
一个更具体的例子:假设接口讨论中的
最后一句警告:小心不要过分强调特性。当独特的类实现足够时,特性常常被用作糟糕设计的拐杖。您应该限制特性以满足最佳代码设计的接口需求。好的。好啊。
接口定义了实现类必须实现的一组方法。
当一个特性是
这是最大的区别。
从PHP RFC的水平重用:
Traits is a mechanism for code reuse in single inheritance languages such as PHP. A Trait is intended to reduce some limitations of single inheritance by enabling a developer to reuse sets of methods freely in several independent classes living in different class hierarchies.
从PHP手册(Emphasis Mine)中:
Traits are a mechanism for code reuse in single inheritance languages such as PHP. ... It is an addition to traditional inheritance and enables horizontal composition of behavior; that is, the application of class members without requiring inheritance.
一个例子:
1 2 3 4 | trait myTrait { function foo() { return"Foo!"; } function bar() { return"Bar!"; } } |
定义了上述特征后,我现在可以执行以下操作:
1 2 3 | class MyClass extends SomeBaseClass { use myTrait; // Inclusion of the trait myTrait } |
此时,当我创建一个类
另外,与许多其他语言一样,PHP使用一个继承模型,这意味着一个类可以从多个接口派生,但不能从多个类派生。然而,一个PHP类可以有多个
需要注意的几点:
1 2 3 4 5 6 7 8 9 | ----------------------------------------------- | Interface | Base Class | Trait | =============================================== > 1 per class | Yes | No | Yes | --------------------------------------------------------------------- Define Method Body | No | Yes | Yes | --------------------------------------------------------------------- Polymorphism | Yes | Yes | No | --------------------------------------------------------------------- |
多态性:
在前面的例子中,当
优先:
如手册所述:
An inherited member from a base class is overridden by a member inserted by a Trait. The precedence order is that members from the current class override Trait methods, which in return override inherited methods.
所以-考虑以下场景:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class BaseClass { function SomeMethod() { /* Do stuff here */ } } interface IBase { function SomeMethod(); } trait myTrait { function SomeMethod() { /* Do different stuff here */ } } class MyClass extends BaseClass implements IBase { use myTrait; function SomeMethod() { /* Do a third thing */ } } |
在上面创建MyClass实例时,会发生以下情况:
结论
我认为
例如:
1 2 3 4 5 6 7 8 9 10 | trait ToolKit { public $errors = array(); public function error($msg) { $this->errors[] = $msg; return false; } } |
您可以在任何使用这个特性的类中使用这个"错误"方法。
1 2 3 4 5 6 7 8 9 10 11 12 | class Something { use Toolkit; public function do_something($zipcode) { if (preg_match('/^[0-9]{5}$/', $zipcode) !== 1) return $this->error('Invalid zipcode.'); // do something here } } |
使用
完全不同!
对于初学者来说,上述答案可能很难理解,这是最简单的理解方法:
特点
1 2 3 4 5 | trait SayWorld { public function sayHello() { echo 'World!'; } } |
所以,如果你想在其他类中使用
1 2 3 4 5 6 7 | class MyClass{ use SayWorld; } $o = new MyClass(); $o->sayHello(); |
太酷了!
不仅函数可以使用特性中的任何东西(函数、变量、常量…)。也可以使用多个特性:
界面
1 2 3 4 5 6 7 8 9 | interface SayWorld { public function sayHello(); } class MyClass implements SayWorld { public function sayHello() { echo 'World!'; } } |
因此,这就是接口与特性的不同之处:您必须在实现类中重新创建接口中的所有内容。接口没有实现。接口只能有函数和常量,不能有变量。
希望这有帮助!
特征只是为了代码重用。
接口只提供要在类中定义的函数的签名,在类中可以使用它,这取决于程序员的判断。因此给我们一组类的原型。
供参考http://www.php.net/manual/en/language.oop5.traits.php
An often used metaphor to describe Traits is Traits are interfaces with implementation.
在大多数情况下,这是一种很好的思考方式,但两者之间存在许多细微的差异。
首先,
现在在PHP中,有一些函数可以让您获得类使用的所有特性的列表,但是特性继承意味着您需要进行递归检查,以可靠地检查某个类在某个时刻是否具有特定的特性(在php doco页面上有示例代码)。但是,它肯定不像instanceof那么简单和干净,imho它是一个可以让PHP变得更好的特性。
此外,抽象类仍然是类,因此它们不能解决与继承相关的多个代码重用问题。记住,您只能扩展一个类(实数或抽象数),但可以实现多个接口。
我发现特性和接口确实很好地结合使用创建伪多重继承。如:
1 2 3 4 5 | class SlidingDoor extends Door implements IKeyed { use KeyedTrait; [...] // Generally not a lot else goes here since it's all in the trait } |
这样做意味着您可以使用InstanceOf来确定特定的门对象是否已设置了键,您知道您将得到一组一致的方法等,并且所有代码都位于使用keyedtrait的所有类的同一位置。
基本上,您可以将特性视为代码的自动"复制粘贴"。
使用特征是危险的,因为在执行前不可能知道它做了什么。
然而,由于缺乏继承等限制,性状更具灵活性。
特征可以用来注入一种方法,这种方法可以检查类中是否存在其他方法或属性。这是一篇很好的文章(但法语,对不起)
对于那些能读到它的法国人来说,GNU/Linux杂志hs 54有一篇关于这个主题的文章。
其他答案在解释界面和特性之间的差异方面做得很好。我将重点介绍一个有用的真实世界示例,特别是一个演示特性可以使用实例变量的示例-允许您使用最少的样板代码向类添加行为。
同样,正如其他人提到的,特性与接口很好地匹配,允许接口指定行为契约,并允许特性实现实现。
在某些代码库中,向类添加事件发布/订阅功能可能是常见的场景。有三种常见的解决方案:
每种方法的效果如何?
#1不好用。直到你意识到你不能扩展基类的那一天,因为你已经在扩展别的东西了。我不会举一个这样的例子,因为很明显,使用这样的继承会有多大的限制。
#2&3都很好地工作。我将展示一个突出一些差异的示例。
首先,两个示例之间的一些代码是相同的:
接口
1 2 3 4 5 | interface Observable { function addEventListener($eventName, callable $listener); function removeEventListener($eventName, callable $listener); function removeAllEventListeners($eventName); } |
以及一些演示用法的代码:
1 2 3 4 5 6 7 8 9 10 11 12 | $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 "; }); // Mock some bids. foreach (['Moe', 'Curly', 'Larry'] as $name) { $auction->addBid($name, rand()); } |
好了,现在让我们展示一下使用特性时,
首先,下面是2(使用合成)的外观:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | 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); } } |
以下是3(特征)的样子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | 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]); } } |
注意,
差别很大。在使用组合时,我们得到了一个很好的解决方案,允许我们通过任意多的类重用
但是,有时您可能不希望
但是,在大多数场景中,这个特性是非常引人注目的,特别是如果接口有很多方法,这会导致您编写大量样板文件。
*实际上,您可以两者兼而有之——定义
如果你懂英语,知道
基本上,您可以将它与单个变量进行比较。闭包函数可以使这些变量从作用域外部来,这样它们就有了内部的值。它们很强大,可以在任何地方使用。如果特性被使用,也会发生同样的情况。
该特性与我们可以用于多个继承目的以及代码可重用性的类相同。
我们可以在类内使用特性,也可以在同一个类中使用带有"use keyword"的多个特性。
接口用于代码可重用性,与特性相同
接口是扩展多个接口的,因此我们可以解决多个继承问题,但是当我们实现接口时,我们应该在类中创建所有方法。有关详细信息,请单击下面的链接:
http://php.net/manual/en/language.oop5.traits.phphttp://php.net/manual/en/language.oop5.interfaces.php
接口是一种契约,它表示"这个对象能够做这件事",而特征是赋予对象做这件事的能力。
特性本质上是在类之间"复制和粘贴"代码的一种方法。
尝试阅读这篇文章
主要的区别在于,对于接口,必须在实现所述接口的每个类中定义每个方法的实际实现,这样可以让许多类实现相同的接口,但具有不同的行为,而特性只是在一个类中注入的代码块;另一个重要的区别是,特性方法只能是类方法或静态方法,与也可以(通常是)实例方法的接口方法不同。