特性与接口


344

我最近一直在尝试研究PHP,但发现自己迷上了特质。我了解水平代码重用的概念,并且不想一定要从抽象类继承。我不明白的是:使用特征和接口之间的关键区别是什么?

我曾尝试搜索一篇不错的博客文章或文章,解释何时使用一种或另一种,但到目前为止,我发现的示例似乎是如此相似,以至于完全相同。


6
接口在功能体内没有任何代码。它们实际上没有任何功能主体。
hakre 2012年

2
尽管我的回答很受好评,但我还是希望它能记录在案,因为我通常是anti-trait / mixin。查看此聊天记录,以了解特质如何经常破坏扎实的OOP实践
rdlowrey

2
我会相反。在特征出现之前和之后,已经与PHP合作多年,我认为很容易证明其价值。只需阅读一下这个实际示例,即可使“图像模型”也像Imagick对象一样走路和说话,而不必花很多时间在特质出现之前的膨胀。
quickshiftin

性状和界面相似。主要区别在于特性允许您实现方法,而接口则不允许。
约翰

Answers:


238

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

当一个特征被使用时use,这些方法的实现也会随之而来-这在Interface。中不会发生。

那是最大的不同。

PHP RFC水平复用

特性是一种在PHP等单一继承语言中重用代码的机制。特性旨在通过使开发人员在生活在不同类层次结构中的几个独立类中自由重用方法集,从而减少单一继承的某些限制。


2
@JREAM实际上,什么也没有。实际上,还有更多。
亚历克峡谷

79
特质根本不是接口。接口是可以检查的规范。无法检查特性,因此它们仅是实现。它们与接口完全相反。RFC中的这一行简直是错误的……
ircmaxell,2012年

195
特性本质上是语言辅助的复制和粘贴
沙希德

10
那不是一个比喻。那只是在扼杀单词的含义。就像将盒子描述为具有体积的表面一样。
cleong 2012年

6
要扩展ircmaxell和Shadi的注释:您可以检查对象是否实现接口(通过instanceof),并且可以确保方法参数通过方法签名中的类型提示来实现接口。您无法对特征进行相应的检查。
Brian D'Astous

530

公共服务声明:

作为记录,我想说一下,我相信特征几乎总是一种代码味道,应该避免使用,以利于组合。我认为单继承经常被滥用为反模式,而多继承只会使这个问题复杂化。在大多数情况下,通过支持组合而不是继承(无论是单个还是多个),将可以为您提供更好的服务。如果您仍然对特征及其与接口的关系感兴趣,请继续阅读...


让我们先说一下:

面向对象编程(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在界面讨论中都使用相同的方法来确定高速缓存条目是否陈旧并应删除(显然,在现实生活中并非如此,但要这样做)。您可以编写一个特征,并允许两个类都将其用于通用接口要求。

最后要提请您注意的一点:小心不要过分追求特质。当独特的类实现就足够了时,特质常常被用作劣质设计的拐杖。您应该将特征限制为满足最佳代码设计的接口要求。


69
我确实在寻找上面提供的快速简单答案,但是我不得不说,您给出了一个很好的深入答案,这将使其他人(荣誉)更加清楚。
datguywhowanders 2012年

35
“ [实现]满足给定类中接口所需功能的特征是一个理想的用例”。恰好:+1
Alec Gorge 2012年

5
公平地说,PHP中的特征与其他语言中的mixins相似吗?
伊诺(Eno)2012年

5
@igorpan对于所有意图和目的,我会说PHP的特点实现一样的多重继承。值得注意的是,如果PHP中的特征指定静态属性,则每个使用该特征的类都将拥有自己的静态属性副本。更重要的是……在查询特质时,看到此帖子现在在SERP上的位置如何很高,我将在页面顶部添加公共服务公告。您应该阅读它。
rdlowrey

3
+1进行深入说明。我来自红宝石背景,很多情况下使用mixins。只需加上我的两分钱,我们使用的一个好的经验法则就可以在php中翻译为“不要实现使$ this改变为特征的方法”。这样可以避免一堆疯狂的调试会话... mixin也不应在将要混入的类中做任何假设(否则,您应使其变得非常清晰,并将依赖关系降至最低限度)。在这方面,我发现您对接口实现特征的想法很好。
m_x

67

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 延伸 SomeBaseClassMyClass 一个实例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的实例时,会发生以下情况:

  1. Interface IBase要求被称为参数的功能SomeMethod()来提供。
  2. 基类BaseClass提供了此方法的实现-满足需要。
  3. 同时trait myTrait提供了一个称为的SomeMethod()无参数函数,函数优先BaseClass-version
  4. class MyClass提供了自己的版本SomeMethod()- 其优先trait-version。

结论

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

4
“特征类似于抽象类的C#概念”不,抽象类是抽象类;这个概念同时存在于PHP和C#中。我将PHP中的特征与由C#中的扩展方法组成的静态类进行比较,删除了基于类型的限制,因为特征几乎可以用于任何类型,而扩展方法只扩展一种类型。
BoltClock

1
很好的评论-我同意你的看法。在重读时,这是一个更好的类比。我相信将其视为一种更好的方法mixin-在我重新打开答案的同时,我也进行了更新以反映这一点。感谢您发表评论,@ BoltClock!
Troy Alford 2014年

1
我认为与c#扩展方法没有任何关系。扩展方法被添加到单个类的类型中(当然,这要遵循类的层次结构),它们的目的是通过附加功能来增强类型,而不是“共享代码”在多个类上而造成混乱。不可比!如果需要重用某些东西,通常意味着它应该拥有自己的空间,例如单独的类,它将与需要通用功能的类相关。实施可能会因设计而异,但大致就是如此。性状只是编写糟糕代码的另一种方法。
索菲娅

一个类可以有多个接口吗?我不确定是否弄错了您的图形,但是X类实现Y,Z是有效的。
Yann Chabot,2013年

26

我认为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接口中,因为根本没有类似于(智能地)将类强制转换为整数的方法。
马修

5
忘记“ to_integer”-只是一个例子。一个例子。一个“你好,世界”。一个“ example.com”。
J. Bruni

2
该工具箱特性提供了一个独立的实用程序类不能提供什么好处?而不是use Toolkit您可能拥有$this->toolkit = new Toolkit();该特性本身,或者我错过了它的某些优势?
安东尼

@Anthony在您Something的容器中的某个位置执行if(!$something->do_something('foo')) var_dump($something->errors);
TheRealChx101

20

对于以上问题的初学者来说可能很难,这是理解它的最简单方法:

特质

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,不能具有变量。

我希望这有帮助!


5

描述特质的一个经常使用的隐喻是特质是实现的接口。

在大多数情况下,这是一种思考的好方法,但是两者之间存在许多细微的差异。

首先,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的所有类中。


当然,答案的最后一部分是@rdlowrey在其帖子中“特质”下的最后三段中所说的更详细的内容;我只是觉得一个非常简单的框架代码片段将有助于说明这一点。
乔恩·克洛斯凯

我认为使用特质的最佳OO方法是尽可能使用接口。并且,如果存在多个子类为该接口实现相同类型代码的情况,并且您无法将该代码移至其(抽象)超类中->使用特质来实现它
玩家一


3

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

使用特征很危险,因为在执行之前没有任何办法知道它的作用。

但是,由于缺乏继承等限制,所以特性更加灵活。

特性对于将检查某些内容的方法注入到类中很有用,例如,另一个方法或属性的存在。关于这一点的一篇不错的文章(但对不起,是法文)

对于能够读懂法语的人,GNU / Linux杂志HS 54上有一篇有关该主题的文章。


仍然不知道特征与具有默认实现的接口有何不同
denis631

@ denis631您可以将特性视为代码段,将接口视为签名合同。如果有帮助,您可以将其视为可以包含任何内容的类的非正式形式。让我知道是否有帮助。

我看到PHP特质可以看作是宏,然后在编译时将其扩展/仅使用该键为该代码片段添加别名。但是,Rust特质似乎有所不同(或者我错了)。但是因为它们都具有单词特质,所以我认为它们是相同的,意味着相同的概念。锈病特征链接:doc.rust-lang.org/rust-by-example/trait.html
denis631 '18

2

如果您会英语,并且知道trait这是什么意思,那么它的名称正是在说。它是一类无类的方法和属性,您可以通过键入将其附加到现有类use

基本上,您可以将其与单个变量进行比较。闭包函数可以将use这些变量从作用域之外,从而使它们具有内部值。它们功能强大,可用于所有方面。如果使用特质,也会发生同样的情况。


2

其他答案在解释界面和特性​​之间的差异方面做得很好。我将集中讨论一个有用的现实示例,特别是一个演示trait可以使用实例变量的示例-允许您以最少的样板代码将行为添加到类中。

再次,就像其他人提到的那样,特征与接口很好地配对,允许接口指定行为契约,并由特征来实现实现。

在某些代码库中,将事件发布/订阅功能添加到类可能是常见的情况。共有3种常见解决方案:

  1. 使用事件发布/子代码定义一个基类,然后想要提供事件的类可以对其进行扩展以获取功能。
  2. 定义一个带有事件发布/订阅代码的类,然后其他想要提供事件的类可以通过组合使用它,定义自己的方法来包装所组成的对象,并将方法调用代理给它。
  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]);
    }
}

请注意,中的代码EventEmitterTraitEventEmitter类中的代码完全相同,除了trait将triggerEvent()方法声明为protected。因此,您需要查看的唯一区别是Auctionclass 的实现

而且差异很大。当使用组合时,我们得到了一个很好的解决方案,使我们可以EventEmitter按自己的意愿重复使用尽可能多的类。但是,主要缺点是我们需要编写和维护许多样板代码,因为对于Observable接口中定义的每个方法,我们都需要实现它并编写无聊的样板代码,该代码仅将参数转发给相应的方法。我们组成了EventEmitter对象。在本示例中使用该特征可以避免这种情况,从而帮助我们减少样板代码并提高可维护性

但是,有时您可能不希望您的Auction类实现完整的Observable接口-可能只希望公开1或2个方法,甚至根本不公开任何方法,以便可以定义自己的方法签名。在这种情况下,您可能仍然更喜欢合成方法。

但是,此特性在大多数情况下都非常引人注目,尤其是在接口包含许多方法的情况下,这会导致您编写大量样板文件。

*实际上,您可以同时做这两种事情-定义EventEmitter类以防万一,您可以使用EventEmitterTrait特质EventEmitter内的类实现来定义特质,也可以定义特质:)


1

特质与我们可以用于多重继承以及代码可重用性的类相同。

我们可以在类中使用特征,也可以在同一类中使用“ use keyword”使用多个特征。

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

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

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



0

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.