我最近一直在尝试研究PHP,但发现自己迷上了特质。我了解水平代码重用的概念,并且不想一定要从抽象类继承。我不明白的是:使用特征和接口之间的关键区别是什么?
我曾尝试搜索一篇不错的博客文章或文章,解释何时使用一种或另一种,但到目前为止,我发现的示例似乎是如此相似,以至于完全相同。
Imagick
对象一样走路和说话,而不必花很多时间在特质出现之前的膨胀。
我最近一直在尝试研究PHP,但发现自己迷上了特质。我了解水平代码重用的概念,并且不想一定要从抽象类继承。我不明白的是:使用特征和接口之间的关键区别是什么?
我曾尝试搜索一篇不错的博客文章或文章,解释何时使用一种或另一种,但到目前为止,我发现的示例似乎是如此相似,以至于完全相同。
Imagick
对象一样走路和说话,而不必花很多时间在特质出现之前的膨胀。
Answers:
接口定义了实现类必须实现的一组方法。
当一个特征被使用时use
,这些方法的实现也会随之而来-这在Interface
。中不会发生。
那是最大的不同。
特性是一种在PHP等单一继承语言中重用代码的机制。特性旨在通过使开发人员在生活在不同类层次结构中的几个独立类中自由重用方法集,从而减少单一继承的某些限制。
公共服务声明:
作为记录,我想说一下,我相信特征几乎总是一种代码味道,应该避免使用,以利于组合。我认为单继承经常被滥用为反模式,而多继承只会使这个问题复杂化。在大多数情况下,通过支持组合而不是继承(无论是单个还是多个),将可以为您提供更好的服务。如果您仍然对特征及其与接口的关系感兴趣,请继续阅读...
让我们先说一下:
面向对象编程(OOP)可能很难理解。仅仅因为您使用类并不意味着您的代码是面向对象(OO)的。
要编写OO代码,您需要了解OOP实际上与对象的功能有关。您必须根据类可以做什么而不是实际做什么来考虑类。这与传统的过程式编程形成了鲜明的对比,在传统的过程式编程中,重点是使一些代码“有所作为”。
如果OOP代码是关于规划和设计的,则接口是蓝图,而对象是完全建造的房屋。同时,特征只是帮助构建蓝图(界面)所布置房屋的一种方法。
那么,为什么要使用接口呢?很简单,接口使我们的代码不那么脆弱。如果您对此声明有疑问,请询问被迫维护未针对接口编写的旧代码的任何人。
接口是程序员与其代码之间的契约。该界面说:“只要您遵守我的规则,就可以按照自己的喜好实现我,并且我保证我不会破坏您的其他代码。”
因此,以一个实际的场景为例(没有汽车或小部件):
您想要为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
类依赖于a 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
在界面讨论中都使用相同的方法来确定高速缓存条目是否陈旧并应删除(显然,在现实生活中并非如此,但要这样做)。您可以编写一个特征,并允许两个类都将其用于通用接口要求。
最后要提请您注意的一点:小心不要过分追求特质。当独特的类实现就足够了时,特质常常被用作劣质设计的拐杖。您应该将特征限制为满足最佳代码设计的接口要求。
A trait
本质上是PHP对a的实现mixin
,并且实际上是一组扩展方法,可以通过添加将该扩展方法添加到任何类中trait
。这些方法将成为该类实现的一部分,但不使用继承。
从PHP手册(重点是我的):
特性是一种在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
-defined方法已经具有方法主体Interface
--defined方法不能。
另外,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
因为a 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的实例时,会发生以下情况:
Interface
IBase
要求被称为参数的功能SomeMethod()
来提供。BaseClass
提供了此方法的实现-满足需要。trait
myTrait
提供了一个称为的SomeMethod()
无参数函数,该函数优先于BaseClass
-versionclass
MyClass
提供了自己的版本SomeMethod()
- 其优先在trait
-version。结论
Interface
不能提供一个方法体的默认实现,同时trait
可以。Interface
是多态,继承结构-而trait
不是。Interface
s可以在同一类中使用,多个s也可以使用trait
。mixin
-在我重新打开答案的同时,我也进行了更新以反映这一点。感谢您发表评论,@ BoltClock!
我认为traits
创建包含可以用作几个不同类的方法的方法的类很有用。
例如:
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
。特征不是这种情况。
完全不同!
to_integer
将更可能包含在IntegerCast
接口中,因为根本没有类似于(智能地)将类强制转换为整数的方法。
use Toolkit
您可能拥有$this->toolkit = new Toolkit();
该特性本身,或者我错过了它的某些优势?
Something
的容器中的某个位置执行if(!$something->do_something('foo')) var_dump($something->errors);
对于以上问题的初学者来说可能很难,这是理解它的最简单方法:
特质
trait SayWorld {
public function sayHello() {
echo 'World!';
}
}
因此,如果您想sayHello
在其他类中具有功能而不重新创建整个功能,则可以使用特征,
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!';
}
}
所以这就是接口与特征的不同之处:您必须在实现的类中的接口中重新创建所有内容。接口没有实现。和接口只能具有函数和const,不能具有变量。
我希望这有帮助!
描述特质的一个经常使用的隐喻是特质是实现的接口。
在大多数情况下,这是一种思考的好方法,但是两者之间存在许多细微的差异。
首先,instanceof
操作员将不会使用特征(即特征不是真实的对象),因此您不能让我们看一看一个类是否具有某个特征(或者看是否两个其他不相关的类共享一个特征) )。这就是将其作为水平代码重用的构造的意思。
PHP中现在有一些函数,可以让您获得类使用的所有特征的列表,但是特征继承意味着您需要进行递归检查,以可靠地检查某个时刻某个类是否具有特定特征(例如: PHP doco页面上的代码)。但是,是的,它肯定不像instanceof那样简单明了,恕我直言,它是使PHP更好的功能。
同样,抽象类仍然是类,因此它们不能解决与多重继承相关的代码重用问题。请记住,您只能扩展一个类(实际或抽象),但可以实现多个接口。
我发现特质和接口真的很不错,可以一起使用来创建伪多重继承。例如:
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的所有类中。
特性只是为了代码重用。
接口仅提供要在类中定义的功能的签名,可以根据程序员的判断使用该接口。这样就给我们提供了一组课程的原型。
基本上,您可以将特征视为自动的代码“复制粘贴”。
使用特征很危险,因为在执行之前没有任何办法知道它的作用。
但是,由于缺乏继承等限制,所以特性更加灵活。
特性对于将检查某些内容的方法注入到类中很有用,例如,另一个方法或属性的存在。关于这一点的一篇不错的文章(但对不起,是法文)。
对于能够读懂法语的人,GNU / Linux杂志HS 54上有一篇有关该主题的文章。
其他答案在解释界面和特性之间的差异方面做得很好。我将集中讨论一个有用的现实示例,特别是一个演示trait可以使用实例变量的示例-允许您以最少的样板代码将行为添加到类中。
再次,就像其他人提到的那样,特征与接口很好地配对,允许接口指定行为契约,并由特征来实现实现。
在某些代码库中,将事件发布/订阅功能添加到类可能是常见的情况。共有3种常见解决方案:
use
将特征(也称为导入)获得功能。各项工作效果如何?
#1运作不佳。直到那天,您意识到无法扩展基类,因为您已经在扩展其他内容。我不会显示一个示例,因为使用这样的继承有多明显是很明显的。
#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());
}
好的,现在让我们展示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将triggerEvent()
方法声明为protected。因此,您需要查看的唯一区别是Auction
class 的实现。
而且差异很大。当使用组合时,我们得到了一个很好的解决方案,使我们可以EventEmitter
按自己的意愿重复使用尽可能多的类。但是,主要缺点是我们需要编写和维护许多样板代码,因为对于Observable
接口中定义的每个方法,我们都需要实现它并编写无聊的样板代码,该代码仅将参数转发给相应的方法。我们组成了EventEmitter
对象。在本示例中使用该特征可以避免这种情况,从而帮助我们减少样板代码并提高可维护性。
但是,有时您可能不希望您的Auction
类实现完整的Observable
接口-可能只希望公开1或2个方法,甚至根本不公开任何方法,以便可以定义自己的方法签名。在这种情况下,您可能仍然更喜欢合成方法。
但是,此特性在大多数情况下都非常引人注目,尤其是在接口包含许多方法的情况下,这会导致您编写大量样板文件。
*实际上,您可以同时做这两种事情-定义EventEmitter
类以防万一,您可以使用EventEmitterTrait
特质EventEmitter
内的类实现来定义特质,也可以定义特质:)
特质与我们可以用于多重继承以及代码可重用性的类相同。
我们可以在类中使用特征,也可以在同一类中使用“ use keyword”使用多个特征。
该接口用于与特征相同的代码可重用性
接口是扩展多个接口,因此我们可以解决多个继承问题,但是当实现接口时,我们应该在类内部创建所有方法。有关更多信息,请单击下面的链接:
http://php.net/manual/en/language.oop5.traits.php http://php.net/manual/en/language.oop5.interfaces.php
主要区别在于,对于接口,您必须在实现该接口的每个类中定义每个方法的实际实现,因此您可以让许多类实现相同的接口,但行为不同,而特征只是注入的代码块一类; 另一个重要的区别是,特征方法只能是类方法或静态方法,而接口方法也可以(通常是实例方法),这与接口方法不同。