性状与接口

最近我一直在试图研究PHP,并发现自己陷入了特性。 我理解横向代码重用的概念,不想从抽象类inheritance。 我不明白的是使用特征和接口之间的关键区别是什么?

我试过寻找一个体面的博客文章或文章解释什么时候使用一个或另一个,但我迄今发现的例子看起来非常相似,是相同的。

任何人都可以在这里分享他们的意见/观点吗?

接口定义了实现类必须实现的一组方法。

当一个特质被use时,方法的实现也会出现 – 这在Interface中不会发生。

这是最大的区别。

从PHP的水平重用 :

特性是单一inheritance语言(如PHP)中的代码重用机制。 特质旨在通过使开发人员能够在生活在不同类层次中的几个独立类中自由地重用一组方法,从而减less单一inheritance的某些限制。

公共服务声明:

我想陈述一下,我相信特质几乎总是一种代码味道,应该避免使用构图。 我认为单一inheritance经常被滥用到反模式的地步,而多重inheritance只会加剧这个问题。 在大多数情况下,通过支持合成而不是inheritance(无论是单个还是多个),你会得到更好的服务。 如果你仍然对特征及其与接口的关系感兴趣,请继续阅读…


我们先说这个:

面向对象编程(OOP)可能是一个难以掌握的范例。 仅仅因为你使用类并不意味着你的代码是面向对象的(OO)。

要编写面向对象代码,你需要明白面向对象是关于你的对象的function。 你必须考虑他们可以做什么而不是实际做什么 。 这与传统的程序编程形成了鲜明的对比,其中重点在于编写一些代码“做某事”。

如果OOP代码是关于规划和devise的,那么接口就是蓝图,而对象就是完全构build的房子。 同时,性格只是一种帮助build立蓝图(界面)布局的方式。

接口

那么,我们为什么要使用接口呢? 非常简单,接口使我们的代码更脆弱。 如果您怀疑这个陈述,那么可以要求那些被迫维护那些没有针对接口编写的遗留代码的人。

界面是程序员和他/她的代码之间的契约。 接口说:“只要你按照我的规则玩游戏,你就可以实现我,不pipe你喜欢什么,我保证我不会破坏你的其他代码。”

举个例子,考虑一个真实世界的场景(没有汽车或小工具):

您想要为Web应用程序实施caching系统以减less服务器负载

您首先编写一个类来caching使用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响应对象中,在执行所有工作以生成实际响应之前,检查caching命中:

 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; } } 

这种方法很好。 但也许几个星期后,你决定要使用基于文件的caching系统,而不是APC。 现在,您必须更改控制器代码,因为您已将控制器编程为使用ApcCacher类的function,而不是用于表示ApcCacher类function的接口。 让我们说,而不是上面你已经使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类来使用接口所需的能力。

这个例子(希望)展示了如何编程到一个接口允许你改变你的类的内部实现,而不用担心如果更改会打破你的其他代码。

性状

另一方面,性状只是一种重用代码的方法。 接口不应该被认为是性格的互斥select。 实际上, 创build满足界面所需function的特性是理想的用例

当多个类共享相同的function(可能由相同的接口指定)时,应该只使用特征。 使用特质来为单个类提供function是没有意义的:只会混淆类的function,而更好的devise会将特征的function转移到相关的类中。

考虑以下特质实现:

 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在界面讨论中使用相同的方法来判断一个caching项是否是陈旧的,应该被删除(显然这不是现实生活中的情况,而是与它一起使用)。 你可以编写一个特性,允许这两个类使用它来满足通用接口要求。

最后要提醒的一点是:注意不要过分注意特质。 当独特的类实现就足够了的时候,特性通常被用作糟糕devise的拐杖。 您应该限制特性来满足最佳代码devise的接口要求。

一个trait本质上是PHP的一个mixin的实现,实际上是一组扩展方法,可以通过添加特性添加到任何类中。 然后这些方法成为该类实现的一部分,但不使用inheritance

从PHP手册 (重点是我的):

特征是单一inheritance语言(如PHP)中的代码重用机制。 …这是对传统inheritance的补充,并使行为的水平组成; 也就是不需要inheritance的类成员的应用。

一个例子:

 trait myTrait { function foo() { return "Foo!"; } function bar() { return "Bar!"; } } 

有了上述特点,我现在可以做到以下几点:

 class MyClass extends SomeBaseClass { use myTrait; // Inclusion of the trait myTrait } 

在这一点上,当我创build一个类MyClass的实例时,它有两个方法,称为foo()bar() – 它来自myTrait 。 而且 – 请注意, trait定义的方法已经有一个方法体 – 这是一个Interface定义的方法所不能的。

另外,PHP和许多其他语言一样使用单一的inheritance模型 – 这意味着一个类可以从多个接口派生,但不能派生多个类。 然而,一个PHP类可以有多个trait包含 – 这允许程序员包含可重用的部分 – 就像它们可能包含多个基类一样。

有几件事要注意:

  ----------------------------------------------- | Interface | Base Class | Trait | =============================================== > 1 per class | Yes | No | Yes | --------------------------------------------------------------------- Define Method Body | No | Yes | Yes | --------------------------------------------------------------------- Polymorphism | Yes | Yes | No | --------------------------------------------------------------------- 

多态性:

在前面的例子中, MyClass 扩展了 SomeBaseClassMyClass SomeBaseClass一个实例。 换句话说,诸如SomeBaseClass[] bases的数组可以包含MyClass实例。 同样,如果MyClass扩展了IBaseInterface ,则一个IBaseInterface[] bases可以包含MyClass实例。 没有这样的多态结构可用于trait – 因为trait本质上就是为了程序员的方便而复制到使用它的每个类中的代码。

优先级:

如手册中所述:

来自基类的inheritance成员被Trait插入的成员覆盖。 优先顺序是来自当前类的成员重写Trait方法,它们在返回时覆盖inheritance的方法。

所以 – 考虑下面的情况:

 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 */ } } 

在上面创buildMyClass的实例时,会发生以下情况:

  1. Interface IBase需要提供一个名为SomeMethod()的无参数函数。
  2. 基类BaseClass提供了这个方法的实现 – 满足需要。
  3. trait myTrait提供了一个名为SomeMethod()无参数函数, 它优先BaseClass -version
  4. MyClass class提供了自己的SomeMethod()版本 – 它优先trait -version。

结论

  1. 一个Interface不能提供一个方法体的默认实现,而一个trait可以。
  2. 一个Interface是一个多态的inheritance的构造 – 而一个trait则不是。
  3. 多个Interface可以在同一个类中使用,多个trait也可以使用。

我认为traits对于创build包含可以用作几个不同类的方法的方法的类很有用。

例如:

 trait ToolKit { public $errors = array(); public function error($msg) { $this->errors[] = $msg; return false; } } 

你可以在任何使用这个特征的类中使用这个“错误”方法。

 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 ,只能声明方法签名,而不能声明其函数的代码。 此外,要使用一个接口,你需要遵循一个层次结构,使用implements 。 性状并非如此。

它是完全不同的!

Traits是一个经常用来描述Traits的隐喻,它是实现的接口。

在大多数情况下,这是一个很好的思考方式,但两者之间有一些细微的差别。

一开始, instanceof操作符将不能用于特征(即特征不是真实的对象),所以你不能让我们看看一个类是否具有某种特征(或者查看两个不相关的类是否共享一个特征特征)。 这就是他们所说的横向代码重用构造。

现在PHP中有一些函数可以让你获得一个类使用的所有特性的列表,但是特征inheritance意味着你需要做recursion检查来可靠地检查某个类的某个特定的特征是否具有特定的特征(例如代码在PHP的doco页面)。 但是,它肯定不像instanceof那样简单和干净,而且恕我直言,这是一个使PHP更好的function。

另外,抽象类仍然是类,所以它们不能解决多重inheritance相关的代码重用问题。 记住,你只能扩展一个类(真实或抽象),但实现多个接口。

我发现特性和接口是非常好的手拉手来创build伪多重inheritance。 例如:

 class SlidingDoor extends Door implements IKeyed { use KeyedTrait; [...] // Generally not a lot else goes here since it's all in the trait } 

这样做意味着你可以使用instanceof来确定特定的Door对象是否被键入,你知道你会得到一个一致的方法等,并且所有的代码都在使用KeyedTrait的所有类中的一个地方。

性状只是代码重用

接口只是提供了可以在类定义的函数的签名 ,这取决于程序员的判断 。 这样给了我们一个class级原型

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

基本上,你可以把特质看作代码的自动“复制粘贴”。

使用Traits是很危险的,因为在执行之前不知道它是什么。

然而,性状因为缺乏inheritance等限制而更为灵活。

注入一个方法来检查某个类是否有用,例如: 另一种方法或属性的存在。 关于这个(但在法国,不好意思)

对于法语阅读的人来说,GNU / Linux Magazine HS 54有一篇关于这个问题的文章。

一个接口是一个契约,说“这个对象能够做这件事情”,而一个特质是给对象的能力来做这件事情。

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

尝试阅读这篇文章

主要区别在于,使用接口,您必须在每个实现了所述接口的类中定义每个方法的实际实现,因此您可以让许多类实现相同的接口但具有不同的行为,而特性仅仅是注入的代码块一类; 另一个重要的区别是特质方法只能是类方法或静态方法,而不像接口方法那样也可以(通常是)实例方法。

如果你懂英文,知道什么trait意思,这正是名字所说的。 这是通过键入use附加到现有类的无类的包和方法。

基本上,你可以把它与一个variables进行比较。 闭包函数可以use这些范围之外的variables,这样它们就具有了内部的价值。 他们是强大的,可以用在一切。 如果正在使用特质,也会发生同样的情况。

其他答案在解释接口和特性之间的差异方面做了很大的工作。 我将重点介绍一个有用的现实世界的例子,特别是一个表明特征可以使用实例variables的例子 – 允许用最less的样板代码向类中添加行为。

同样,正如其他人所提到的,特征与接口配对良好,允许接口指定行为契约,以及完成实现的特征。

将事件发布/订阅function添加到类中可能是一些代码库中的常见情况。 有3个常见的解决scheme:

  1. 用事件发布/订阅代码定义一个基类,然后想要提供事件的类可以扩展它以获得这些function。
  2. 定义一个带有事件发布/子代码的类,然后其他想要提供事件的类可以通过组合来使用它,定义它们自己的方法来包装组合对象,代理对它的方法调用。
  3. 定义一个具有事件发布/子代码的特征,然后其他想要提供事件的类可以use这个特征来导入它,以获得这些特征。

每个工作有多好?

#1行不通。 直到你意识到的那一天,你不能扩展基类,因为你已经扩展了别的东西。 我不会举这个例子,因为它应该很明显,限制它是如何使用inheritance。

#2和#3都运作良好。 我将展示一个突出显示一些差异的例子。

首先,两个例子中的一些代码是相同的:

一个界面

 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); } } 

以下是#3(特征)的样子:

 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 EventEmitter triggerEvent()方法声明为protected。 所以, 唯一需要考虑的是Auction类的实现

而且差别很大。 当使用组合时,我们得到了一个很好的解决scheme,使我们可以通过尽可能多的类来重用我们的EventEmitter 。 但是,主要的缺点是我们需要编写和维护大量的样板代码,因为对于在Observable接口中定义的每个方法,我们需要实现它并编写无聊的样板代码,只是将参数转发到相应的方法在我们组成的EventEmitter对象中。 在这个例子中使用特质可以避免这种情况 ,帮助我们减less样板代码并提高可维护性

但是,有些时候您不希望您的Auction类实现完整的Observable接口 – 也许您只想公开1或2个方法,或者甚至可以不提供任何方法,以便您可以定义自己的方法签名。 在这种情况下,您可能仍然更喜欢构图方法。

但是,在大多数情况下,这个特征是非常有吸引力的,特别是如果接口有很多方法,这会导致你写大量的样板。

*你实际上可以这样做 – 定义EventEmitter类,以防万一你想组合使用它,并定义EventEmitterTrait特性,使用特性内的EventEmitter类实现:)