特征与界面

最近我一直在努力学习PHP,我发现自己被trait缠住了。我理解横向代码重用的概念,并且不希望必然地继承抽象类。我不明白的是:使用特征和使用界面之间的关键区别是什么?

我曾试着搜索过一篇像样的博客文章或文章,解释什么时候使用其中一种或另一种,但到目前为止,我找到的例子似乎非常相似,甚至完全相同。

127753 次浏览

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

当一个trait是__abc0d时,方法的实现也会出现——这在Interface中不会发生。

这是最大的不同。

PHP RFC的水平重用:

trait是一种在单继承语言(如PHP)中代码重用的机制。Trait旨在通过允许开发人员在不同类层次结构中的几个独立类中自由地重用方法集来减少单个继承的一些限制。

我认为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中,你只能声明方法签名,而不能声明它的函数代码。此外,要使用接口,你需要遵循一个层次结构,使用implements。但特质却不是这样。

这是完全不同的!

公共服务公告:

我想要声明的是,我相信trait几乎总是一种代码气味,应该避免使用组合。在我看来,单继承经常被滥用到反模式的地步,而多重继承只会加剧这个问题。在大多数情况下,使用组合而不是继承(无论是单个还是多个)会更好。如果您仍然对特征及其与界面的关系感兴趣,请继续阅读…


让我们这样开始:

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

要编写OO代码,你需要理解OOP实际上是关于对象的功能。你必须根据它们< em > < / em >能做而不是它们< em >做< / em >来考虑类。这与传统的过程式编程形成了鲜明的对比,在传统的过程式编程中,重点是让一小段代码“做一些事情”。

如果面向对象编程是关于规划和设计的,那么接口就是蓝图,对象就是完全建成的房子。与此同时,特征只是一种帮助建造蓝图(界面)所布置的房子的方法。

接口

那么,我们为什么要使用接口呢?很简单,接口使我们的代码不那么脆弱。如果您怀疑这种说法,可以问问那些被迫维护遗留代码的人,这些代码不是针对接口编写的。

接口是程序员和他/她的代码之间的契约。这个界面说:“只要你遵守我的规则,你可以随心所欲地实现我,我保证不会破坏你的其他代码。”

举个例子,考虑一个现实世界的场景(没有汽车或小部件):

你想为一个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类来使用接口所需的功能。

这个示例(希望)演示了如何通过接口编程来更改类的内部实现,而不用担心这些更改是否会破坏其他代码。

特征

另一方面,trait只是一种重用代码的方法。界面不应该被认为是与特征相排斥的选择。事实上,创建满足接口所需功能的特征是理想用例

只有当多个类共享相同的功能(可能由相同的接口决定)时,才应该使用trait。使用trait为单个类提供功能是没有意义的:这只会混淆类的功能,更好的设计应该将trait的功能移到相关的类中。

考虑下面的trait实现:

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,并允许两个类使用它来满足公共接口需求。

最后提醒一句:注意不要在性格特征上走极端。当独特的类实现就足够了的时候,特征常常被用作糟糕设计的拐杖。为了获得最佳的代码设计,你应该限制特征以满足界面需求。

描述trait的一个常用比喻是:trait是带有实现的接口。

在大多数情况下,这是一种很好的思考方式,但两者之间存在一些微妙的差异。

首先,instanceof操作符不适用于trait(即,trait不是一个真实的对象),因此不能使用它来查看一个类是否具有某个trait(或者查看两个不相关的类是否共享某个trait)。这就是他们所说的横向代码重用的结构。

现在PHP中有函数可以让你获得一个类使用的所有trait的列表,但是trait继承意味着你需要做递归检查来可靠地检查一个类是否在某一点上具有特定的trait (PHP doco页面上有示例代码)。但是,是的,它肯定不像instanceof那么简单和干净,恕我直言,它是一个可以让PHP更好的特性。

而且,抽象类仍然是类,因此它们不能解决与多重继承相关的代码重用问题。请记住,您只能扩展一个类(真实的或抽象的),但可以实现多个接口。

我发现trait和接口非常适合一起使用来创建伪多重继承。例如:

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的类中都在一个地方。

基本上,您可以将trait视为代码的自动“复制-粘贴”。

使用trait是很危险的,因为在执行前我们无法知道它的作用。

然而,由于缺乏遗传等限制,性状更加灵活。

trait在注入方法时很有用,它可以检查类中是否存在另一个方法或属性。这是一篇不错的文章(抱歉,是用法语写的)

对于能读法语的人来说,GNU/Linux杂志HS 54有一篇关于这个主题的文章。

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

来自PHP手册(强调我的):

trait是单继承语言(如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。并且——注意,__abc4定义的方法已经有一个方法体——这是__abc5定义的方法所没有的。

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

有几件事需要注意:

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

多态:

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

优先级:

如手册所述:

从基类继承的成员被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的实例时,会发生以下情况:

  1. Interface IBase需要提供一个名为SomeMethod()的无参数函数。
  2. 基类BaseClass提供了此方法的实现-满足需求。
  3. trait myTrait也提供了一个名为SomeMethod()的无参数函数,在__abc3版本之上提供了这是优先的
  4. class MyClasstrait-version之上提供了自己的SomeMethod() - 这是优先的版本。

结论

  1. Interface不能提供方法体的默认实现,而trait可以。
  2. Interface是一个多态继承了结构,而trait不是。
  3. 同一个类中可以使用多个__abc0,也可以使用多个__abc1。

特征是简单的代码重用

接口只是提供了将要成为在类中定义的函数的签名,它可以根据程序员的自由裁量权使用。这样就给了我们一组类原型

< p >,供参考 http://www.php.net/manual/en/language.oop5.traits.php < / p >

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

接口是一种契约,它表明“这个对象能够做这件事”,而trait则赋予对象做这件事的能力。

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

试着阅读这篇文章, PHP的特征是什么?< / em > < / >

如果你懂英语并且知道trait的意思,它就是这个名字的意思。它是一个无类的方法和属性包,通过键入use附加到现有的类。

基本上,你可以将它与单个变量进行比较。闭包函数可以从作用域外部use这些变量,这样它们就有了内部的值。他们是强大的,可以在任何地方使用。如果trait被使用,也会发生同样的情况。

其他答案很好地解释了界面和特征之间的差异。我将重点介绍一个有用的真实例子,特别是一个演示trait可以使用实例变量的例子——允许您用最少的样板代码向类添加行为。

再一次,像其他人提到的那样,特征与接口很好地配对,允许接口指定行为契约,而特征则完成实现。

在一些代码库中,向类添加事件发布/订阅功能是常见的场景。有3种常见的解决方案:

  1. 定义带有事件发布/订阅代码的基类,然后希望提供事件的类可以扩展它以获得功能。
  2. 定义一个带有事件发布/订阅代码的类,然后其他想要提供事件的类可以通过组合来使用它,定义自己的方法来包装组合对象,将方法调用代理给它。
  3. 用事件发布/订阅代码定义trait,然后其他想要提供事件的类可以use该trait,也就是导入它,以获得功能。

它们的工作效果如何?

第一条效果不好。它会,直到有一天你意识到你不能扩展基类,因为你已经扩展了其他的东西。我将不展示这方面的示例,因为这样使用继承的局限性应该是显而易见的。

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

好了,现在让我们来展示在使用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类的实现

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

然而,有时你可能不希望你的Auction类实现完整的Observable接口——也许你只想公开1或2个方法,甚至可能根本不公开,这样你就可以定义自己的方法签名。在这种情况下,您可能仍然喜欢组合方法。

但是,在大多数情况下,这个特性非常引人注目,特别是当接口有很多方法时,这会导致您编写大量的样板文件。

*你实际上可以两者都做——定义EventEmitter类以防你想组合使用它,也定义EventEmitterTrait trait,使用trait内部的EventEmitter类实现:)

对于初学者来说,上面的答案可能很难,下面是最简单的理解方法:

特征

trait SayWorld {
public function sayHello() {
echo 'World!';
}
}

所以如果你想在其他类中有sayHello函数而不重新创建整个函数,你可以使用trait,

class MyClass{
use SayWorld;


}


$o = new MyClass();
$o->sayHello();

酷吧!

不只是函数,你可以使用trait中的任何东西(function, variables, const…)另外,你可以使用多个trait: use SayWorld, AnotherTraits;

接口

  interface SayWorld {
public function sayHello();
}


class MyClass implements SayWorld {
public function sayHello() {
echo 'World!';
}
}

因此,这就是接口与特征的不同之处:您必须在实现的类中重新创建接口中的所有内容。接口没有实现,接口只能有函数和常量,不能有变量。

我希望这能有所帮助!

该特征与我们可以用于多重继承目的和代码可重用性的类相同。

我们可以在类中使用trait,也可以在同一个类中使用'use keyword'来使用多个trait。

接口用于代码可重用性与特性相同

接口是扩展多个接口,所以我们可以解决多个继承问题,但当我们实现接口时,我们应该在类中创建所有的方法。 更多信息点击下面的链接:

http://php.net/manual/en/language.oop5.traits.php http://php.net/manual/en/language.oop5.interfaces.php < / p >