关于PHP:特性与接口Traits vs. Interfaces

Traits vs. Interfaces

最近我一直在努力学习PHP,我发现自己已经被特性挂掉了。我理解水平代码重用的概念,不希望从抽象类继承。我不明白的是,使用特性和界面之间的关键区别是什么?

我试着找一篇像样的博客文章或文章来解释什么时候使用一个或另一个,但到目前为止,我发现的例子似乎非常相似,以致于完全相同。

有人能分享他们对此的看法吗?


公共服务公告:好的。

我想声明的是,我相信特性几乎总是一种代码味道,应该避免有利于合成。我的观点是,单一继承经常被滥用到反模式的地步,多重继承只会加剧这个问题。在大多数情况下,通过支持组合而不是继承(无论是单继承还是多继承),您会得到更好的服务。如果你仍然对特性及其与界面的关系感兴趣,请继续阅读……好的。

我们先说:好的。

Object-Oriented Programming (OOP) can be a difficult paradigm to grasp.
Just because you're using classes doesn't mean your code is
Object-Oriented (OO).

Ok.

要编写OO代码,您需要了解OOP实际上是关于对象的功能。你必须从课程能做什么而不是他们实际做什么的角度来考虑课程。这与传统的程序化编程形成了鲜明的对比,传统程序化编程的重点是让代码"做点什么"。好的。

如果OOP代码是关于规划和设计的,那么一个接口就是蓝图,一个对象就是完全构建的房子。同时,特征只是帮助建造蓝图(界面)布置的房子的一种方法。好的。界面

那么,我们为什么要使用接口呢?很简单,接口使我们的代码不那么脆弱。如果您怀疑这个声明,请询问任何被迫维护不是针对接口编写的遗留代码的人。好的。

接口是程序员和他/她的代码之间的契约。界面说,"只要你按照我的规则玩,你就可以随心所欲地实现我,我保证不会破坏你的其他代码。"好的。

因此,作为一个例子,考虑一个真实的场景(没有汽车或小部件):好的。

You want to implement a caching system for a web application to cut
down on server load

Ok.

首先,使用apc将类写入缓存请求响应:好的。

1
2
3
4
5
6
7
8
9
10
11
12
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响应对象中,在执行所有生成实际响应的工作之前检查缓存命中:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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阶级:好的。

1
2
// your controller's constructor using the interface as a dependency
public function __construct(Request $req, Response $resp, CacherInterface $cacher=NULL)

要遵循这一点,您可以这样定义接口:好的。

1
2
3
4
5
6
interface CacherInterface
{
  public function fetch($key);
  public function store($key, $data);
  public function delete($key);
}

反过来,您的ApcCacher和新的FileCacher类都实现了CacherInterface,并且您对Controller类编程以使用接口所需的功能。好的。

这个例子(希望)演示了如何编程到一个接口,允许您更改类的内部实现,而不必担心更改是否会破坏其他代码。好的。特点

另一方面,特性只是一种重新使用代码的方法。接口不应该被认为是特性的互斥替代品。事实上,创建满足接口所需功能的特性是理想的用例。好的。

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

考虑以下特性实现:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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!';
    }
}

一个更具体的例子:假设接口讨论中的FileCacherApcCacher都使用相同的方法来确定缓存条目是否过时,是否应该删除(显然现实生活中不是这样,但请继续)。您可以编写一个特性,并允许两个类使用它来满足公共接口需求。好的。

最后一句警告:小心不要过分强调特性。当独特的类实现足够时,特性常常被用作糟糕设计的拐杖。您应该限制特性以满足最佳代码设计的接口需求。好的。好啊。


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

当一个特性是use时,这些方法的实现也会出现——这在Interface中不会发生。

这是最大的区别。

从PHP RFC的水平重用:

Traits is a mechanism for code reuse in single inheritance languages such as PHP. A Trait is intended to reduce some limitations of single inheritance by enabling a developer to reuse sets of methods freely in several independent classes living in different class hierarchies.


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

从PHP手册(Emphasis Mine)中:

Traits are a mechanism for code reuse in single inheritance languages such as PHP. ... It is an addition to traditional inheritance and enables horizontal composition of behavior; that is, the application of class members without requiring inheritance.

一个例子:

1
2
3
4
trait myTrait {
    function foo() { return"Foo!"; }
    function bar() { return"Bar!"; }
}

定义了上述特征后,我现在可以执行以下操作:

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

此时,当我创建一个类MyClass的实例时,它有两个方法,称为foo()bar(),它们来自myTrait。注意,trait定义的方法已经有一个方法体,而Interface定义的方法不能。

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

需要注意的几点:

1
2
3
4
5
6
7
8
9
                      -----------------------------------------------
                      |   Interface   |  Base Class   |    Trait    |
                      ===============================================
> 1 per class         |      Yes      |       No      |     Yes     |
---------------------------------------------------------------------
Define Method Body    |      No       |       Yes     |     Yes     |
---------------------------------------------------------------------
Polymorphism          |      Yes      |       Yes     |     No      |
---------------------------------------------------------------------

多态性:

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

优先:

如手册所述:

An inherited member from a base class is overridden by a member inserted by a Trait. The precedence order is that members from the current class override Trait methods, which in return override inherited methods.

所以-考虑以下场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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实例时,会发生以下情况:

  • InterfaceIBase要求提供一个名为SomeMethod()的无参数函数。
  • 基类BaseClass提供了此方法的实现—满足了需要。
  • traitmyTrait也提供了一个名为SomeMethod()的无参数函数,它优先于BaseClass版本。
  • classMyClass提供了自己的SomeMethod()版本,优先于trait版本。
  • 结论

  • Interface不能提供方法体的默认实现,而trait可以。
  • Interface是一种多态的遗传结构,而trait不是。
  • 多个Interfaces可以在同一类中使用,多个traits也可以。

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

    例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    trait ToolKit
    {
        public $errors = array();

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

    您可以在任何使用这个特性的类中使用这个"错误"方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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遵循层次结构。这与性格特征不同。

    完全不同!


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

    特点

    1
    2
    3
    4
    5
    trait SayWorld {
        public function sayHello() {
            echo 'World!';
        }
    }

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

    1
    2
    3
    4
    5
    6
    7
    class MyClass{
      use SayWorld;

    }

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

    太酷了!

    不仅函数可以使用特性中的任何东西(函数、变量、常量…)。也可以使用多个特性:use SayWorld,AnotherTraits;

    界面

    1
    2
    3
    4
    5
    6
    7
    8
    9
      interface SayWorld {
         public function sayHello();
      }

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

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

    希望这有帮助!


    特征只是为了代码重用。

    接口只提供要在类中定义的函数的签名,在类中可以使用它,这取决于程序员的判断。因此给我们一组类的原型。

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


    An often used metaphor to describe Traits is Traits are interfaces with implementation.

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

    首先,instanceof操作符不会处理特性(即特性不是真正的对象),因此我们无法看到一个类是否具有某个特性(或者查看两个不相关的类是否共享一个特性)。这就是他们所说的水平代码重用构造。

    现在在PHP中,有一些函数可以让您获得类使用的所有特性的列表,但是特性继承意味着您需要进行递归检查,以可靠地检查某个类在某个时刻是否具有特定的特性(在php doco页面上有示例代码)。但是,它肯定不像instanceof那么简单和干净,imho它是一个可以让PHP变得更好的特性。

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

    我发现特性和接口确实很好地结合使用创建伪多重继承。如:

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

    这样做意味着您可以使用InstanceOf来确定特定的门对象是否已设置了键,您知道您将得到一组一致的方法等,并且所有代码都位于使用keyedtrait的所有类的同一位置。


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

    使用特征是危险的,因为在执行前不可能知道它做了什么。

    然而,由于缺乏继承等限制,性状更具灵活性。

    特征可以用来注入一种方法,这种方法可以检查类中是否存在其他方法或属性。这是一篇很好的文章(但法语,对不起)

    对于那些能读到它的法国人来说,GNU/Linux杂志hs 54有一篇关于这个主题的文章。


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

    同样,正如其他人提到的,特性与接口很好地匹配,允许接口指定行为契约,并允许特性实现实现。

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

  • 用事件发布/子代码定义一个基类,然后想要提供事件的类可以扩展它以获得功能。
  • 用事件发布/子代码定义一个类,然后其他想要提供事件的类可以通过组合使用它,定义它们自己的方法来包装组合的对象,代理对它的方法调用。
  • 用事件发布/子代码定义一个特性,然后其他想要提供事件的类可以使用特性(也称为导入特性)来获得能力。
  • 每种方法的效果如何?

    #1不好用。直到你意识到你不能扩展基类的那一天,因为你已经在扩展别的东西了。我不会举一个这样的例子,因为很明显,使用这样的继承会有多大的限制。

    #2&3都很好地工作。我将展示一个突出一些差异的示例。

    首先,两个示例之间的一些代码是相同的:

    接口

    1
    2
    3
    4
    5
    interface Observable {
        function addEventListener($eventName, callable $listener);
        function removeEventListener($eventName, callable $listener);
        function removeAllEventListeners($eventName);
    }

    以及一些演示用法的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    $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
    "
    ;
    });

    // Mock some bids.
    foreach (['Moe', 'Curly', 'Larry'] as $name) {
        $auction->addBid($name, rand());
    }

    好了,现在让我们展示一下使用特性时,Auction类的实现是如何不同的。

    首先,下面是2(使用合成)的外观:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    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(特征)的样子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    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类中的代码完全相同,只是特性声明triggerEvent()方法受保护。所以,您需要看到的唯一区别是Auction类的实现。

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

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

    但是,在大多数场景中,这个特性是非常引人注目的,特别是如果接口有很多方法,这会导致您编写大量样板文件。

    *实际上,您可以两者兼而有之——定义EventEmitter类以防您想组合使用它,并定义EventEmitterTrait特性,使用特性内部的EventEmitter类实现:)


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

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


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

    我们可以在类内使用特性,也可以在同一个类中使用带有"use keyword"的多个特性。

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

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

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


    接口是一种契约,它表示"这个对象能够做这件事",而特征是赋予对象做这件事的能力。

    特性本质上是在类之间"复制和粘贴"代码的一种方法。

    尝试阅读这篇文章


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