我最近一直在努力学习PHP,但是我发现我对traits这个概念有些困惑。我理解水平代码复用的概念,以及不一定要从抽象类中继承的想法。但我不明白的是:使用traits和使用接口之间的关键区别是什么?
我尝试搜索一些不错的博客文章或者是解释何时使用其中一个而不是另一个的文章,但是到目前为止我找到的例子都很相似,甚至可以说是一模一样。
公共服务通告:
我要声明的是,我认为traits几乎总是一种代码异味,应该避免使用继承,而是使用组合。在我看来,单一继承经常被滥用到成为反模式的程度,而多重继承只会加剧这个问题。在大多数情况下,通过偏好组合而不是继承(无论是单一还是多重)可以更好地为您服务。如果您仍然对traits及其与接口的关系感兴趣,请继续阅读...
让我们从这里开始:
面向对象编程(OOP)可能是一个难以理解的范例。 仅仅因为你正在使用类并不意味着你的代码是 面向对象(OO)的。
要编写OO代码,您需要了解OOP实际上是关于您的对象能力的。您必须考虑类的能力,而不是它们实际执行的操作。这与传统的过程化编程形成鲜明对比,后者的重点是使代码“做某事”。
如果OOP代码是关于规划和设计的,那么接口就是蓝图,对象就是完全构建的房屋。而traits只是一种帮助按照蓝图(接口)构建房屋的方法。
那么,为什么我们要使用接口呢?简单地说,接口使我们的代码更加健壮。如果您对此表示怀疑,请问任何被迫维护未针对接口编写的遗留代码的人。
接口是程序员和他/她的代码之间的合同。接口说:“只要你按照我的规则来实现我,你可以随意实现我,并且我保证不会破坏你的其他代码。”
因此,作为一个例子,考虑一个真实的场景(没有汽车或小部件):
你想为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
类来使用接口所需的功能。
这个例子(希望如此)演示了通过编写针对接口进行的程序设计,你可以更改类的内部实现而不用担心更改可能会破坏其他代码的问题。
另一方面,特性只是一种重用代码的方法。不应该认为接口是特性的相互排斥的替代品。事实上,创建满足接口所需功能的特性是理想的用例。
只有当多个类共享相同功能(可能由同一接口指定)时,才应使用特性。对于单个类提供功能,使用特性没有意义:这只会混淆类的作用,更好的设计是将特性的功能移入相关类中。
考虑以下特性实现:
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,并允许两个类都使用它来满足共同的接口需求。
最后提醒一点:要小心不要过度使用 traits。通常,当独特的类实现足够时,traits 会被用作设计不佳的替代品。您应将 traits 限制为实现最佳代码设计的接口要求。
一个接口定义了一组方法,实现类必须实现这些方法。
当一个特质被use
后,方法的实现也会随之传递,而这在接口中不会发生。
这是最大的区别。
来自PHP横向重用RFC:
对于单继承语言(如PHP)中的代码重用,特质是一种机制。 特质旨在通过使开发人员能够在不同的类层次结构中自由地重复使用一组方法,从而减少单继承的某些限制。
trait
本质上是PHP实现的mixin
,它是一组扩展方法。通过添加trait
可以将其添加到任何类中,从而使这些方法成为该类实现的一部分,但不使用继承。
来自PHP手册(强调我的):
Traits
是在PHP等单继承语言中进行代码重用的机制。...... 它是传统继承的补充,并使行为的水平组合成为可能;也就是说,应用类成员而无需继承。
示例:
trait myTrait {
function foo() { return "Foo!"; }
function bar() { return "Bar!"; }
}
有了上述定义的特性,现在我可以执行以下操作:
class MyClass extends SomeBaseClass {
use myTrait; // Inclusion of the trait myTrait
}
此时,当我创建一个 MyClass
类的实例时,它有两个方法,分别称为 foo()
和 bar()
- 它们来自于 myTrait
。请注意,trait
定义的方法已经有了方法体,而 Interface
定义的方法则没有。
另外,像许多其他语言一样,PHP 使用单继承模型,这意味着类可以从多个接口派生,但不可以从多个类派生。但是,在 PHP 中,一个类可以包含多个 trait
,这允许程序员包含可重用的代码片段,就像包含多个基类一样。
需要注意的几点:
-----------------------------------------------
| Interface | Base Class | Trait |
===============================================
> 1 per class | Yes | No | Yes |
---------------------------------------------------------------------
Define Method Body | No | Yes | Yes |
---------------------------------------------------------------------
Polymorphism | Yes | Yes | No |
---------------------------------------------------------------------
多态性:
在早期的示例中,MyClass
扩展了 SomeBaseClass
,因此MyClass
是 SomeBaseClass
的一个实例。换句话说,像SomeBaseClass[] bases
这样的数组可以包含MyClass
的实例。同样地,如果MyClass
扩展了 IBaseInterface
,那么IBaseInterface[] bases
数组可以包含MyClass
的实例。使用trait
时没有这样的多态构造 - 因为trait
本质上只是为方便程序员而被复制到每个使用它的类中的代码。
优先级:
正如手册所述:
从基类继承的成员将被插入Trait的成员覆盖。 优先顺序是当前类的成员覆盖Trait方法,这些方法再覆盖继承的方法。
因此 - 考虑以下情况:
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 实例时,会发生以下情况:我认为traits
对于创建包含方法的类,这些方法可以作为多个不同类的方法使用非常有用。
例如:
trait ToolKit
{
public $errors = array();
public function error($msg)
{
$this->errors[] = $msg;
return false;
}
}
您可以在任何使用此特征的类中拥有并使用此“error”方法。
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
}
}
与接口(interfaces
)不同的是,使用特征(traits
)只能声明方法签名,而不能声明其函数代码。此外,要使用接口,您需要遵循层次结构,使用 implements
。但这并不适用于特征。
它完全不同!
to_integer
更可能被包含在 IntegerCast
接口中,因为没有根本上类似的方法可以(智能地)将类转换为整数。 - Matthew$this->toolkit = new Toolkit();
代替 use Toolkit
,或者我错过了 trait 本身的一些好处吗? - AnthonySomething
容器中,你执行了 if(!$something->do_something('foo')) var_dump($something->errors);
。 - TheRealChx101对于初学者来说,以上答案可能有些难以理解,以下是最简单的理解方法:
特征
trait SayWorld {
public function sayHello() {
echo 'World!';
}
}
如果你想在其他类中使用sayHello
函数,而不必重新创建整个函数,那么你可以使用traits。
class MyClass{
use SayWorld;
}
$o = new MyClass();
$o->sayHello();
酷毙了!
在特质中,不仅可以使用函数,还可以使用变量、常量等任何东西。另外,你还可以使用多个特质:use SayWorld, AnotherTraits;
接口
interface SayWorld {
public function sayHello();
}
class MyClass implements SayWorld {
public function sayHello() {
echo 'World!';
}
}
这就是接口与特质之间的不同之处:您必须在已实现的类中重新创建接口中的所有内容。接口没有实现,只能有函数和常量,不能有变量。
希望对您有所帮助!
Traits只是为了代码重用而存在。
接口仅提供函数的签名,这些函数在使用它的类中定义,取决于程序员的决定。因此,为一组类提供了一个原型。
class SlidingDoor extends Door implements IKeyed
{
use KeyedTrait;
[...] // Generally not a lot else goes here since it's all in the trait
}
这样做意味着您可以使用instanceof
来确定特定的门对象是否为 Keyed。您知道您将获得一组一致的方法,所有代码都在使用 KeyedTrait 的所有类中的一个地方。
你可以把 trait 看作是自动化的“复制粘贴”代码。
使用 trait 是危险的,因为在执行之前无法知道它的作用。
然而,由于缺乏类似继承的限制,trait 更加灵活。
例如,traits 可以注入一个检查类中某个方法或属性是否存在的方法。这是一篇关于此主题的好文章(但是是法语)。
对于那些能够阅读法语的人来说,GNU/Linux Magazine HS 54 上有一篇关于此主题的文章。
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());
}
好的,现在让我们展示一下使用traits时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
类的实现。EventEmitter
来重用它。但是,主要缺点是我们需要编写和维护大量样板代码,因为对于在 Observable
接口中定义的每个方法,我们都需要实现它并编写令人厌烦的样板代码,只需将参数转发到我们所组合的 EventEmitter
对象中的相应方法即可。在该示例中使用 trait 可以帮助我们避免这种情况,从而帮助我们减少样板代码并提高可维护性。Auction
类实现完整的 Observable
接口 - 也许您只想公开 1 或 2 个方法,或者甚至根本不想公开方法以便定义自己的方法签名。在这种情况下,您可能仍然更喜欢组合方法。EventEmitter
类以便在需要使用组合时使用它,并定义 EventEmitterTrait
trait,使用 trait 中的 EventEmitter
类实现 :)
Imagick
对象一样行走和交谈,而不需要在 traits 出现之前所需的所有臃肿代码。 - quickshiftin