特征与接口

时间:2012-02-09 03:40:22

标签: php interface traits

我最近一直在尝试研究PHP,而且我发现自己陷入了特征。我理解水平代码重用的概念,而不是必须从抽象类继承。我不明白的是,使用特征与接口之间的关键区别是什么?

我已经尝试过搜索一篇体面的博客文章或解释何时使用其中一篇的文章,但到目前为止我发现的例子看起来非常相似而且完全相同。

有人可以分享他们对此的意见/看法吗?

13 个答案:

答案 0 :(得分:496)

公共服务公告:

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


让我们开始说:

  

面向对象编程(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类依赖于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类进行编程以使用界面所需的功能。< / p>

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

性状

另一方面,Traits只是一种重用代码的方法。不应将界面视为特征的互斥替代品。事实上, 创建满足界面所需功能的特征是理想的用例

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

考虑以下特征实施:

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使用相同的方法来确定缓存条目是否过时且应该删除(显然这不是现实生活中的案例,但与之相伴)。您可以编写一个特征并允许两个类使用它来满足通用接口要求。

最后要提醒的一点:小心不要过分关注特质。当独特的类实现就足够了时,通常将traits用作糟糕设计的拐杖。您应该限制特性以满足最佳代码设计的接口要求。

答案 1 :(得分:223)

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

当特征为use时,方法的实现也会出现 - 这在Interface中不会发生。

这是最大的不同。

来自Horizontal Reuse for PHP RFC

  

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

答案 2 :(得分:61)

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

来自PHP Manual(强调我的):

  

Traits是一种在单继承语言(如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定义的方法已经有一个方法体 - Interface定义的方法不能。

此外 - 与许多其他语言一样,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没有这样的多态结构 - 因为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()的无参数函数,优先于<{strong>> BaseClass - 版本
  4. class MyClass提供了自己的SomeMethod() - 版本,优先于trait版本
  5. <强>结论

    1. Interface无法提供方法正文的默认实现,而trait则可以。
    2. Interface多态继承的构造 - 而trait则不是。
    3. 多个Interface可以在同一个班级中使用,因此可以使用多个trait

答案 3 :(得分:25)

我认为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来遵循层次结构。特征不是这种情况。

完全不同!

答案 4 :(得分:18)

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

<强>性状

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

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

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 :(得分:4)

  

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

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

首先,instanceof运算符不适用于特征(即特征不是真实对象)所以你不能让我们知道一个类是否具有某种特征(或者看到如果两个不相关的类共享一个特征)。这就是它们作为水平代码重用的构造的意思。

现在PHP中的函数可以让你获得一个类使用的所有特征的列表,但特征继承意味着你需要进行递归检查以可靠地检查一个类在某些时候有一个特定的特征(PHP doco页面上有示例代码)。但是,它肯定不像instanceof那样简单和干净,恕我直言,这是一个让PHP更好的功能。

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

我发现traits和接口非常适合用手创建伪多重继承。例如:

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。

答案 6 :(得分:4)

特征仅用于代码重用

界面只提供中定义的函数的签名,可以根据使用它>程序员自行决定。因此,为 一组课程 提供原型

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

答案 7 :(得分:3)

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

使用Traits是危险的,因为在执行之前没有必要知道它的作用。

然而,由于缺乏继承等限制,Traits更加灵活。

特征可以用于注入一种将某些东西检查到类中的方法,例如。另一种方法或属性的存在。 A nice article on that (but in French, sorry)

对于能够获得它的法语读者,GNU / Linux Magazine HS 54有一篇关于这个主题的文章。

答案 8 :(得分:2)

如果你懂英语并知道trait的含义,那正是名字所说的。它是通过键入use附加到现有类的无类方法和属性包。

基本上,您可以将它与单个变量进行比较。闭包函数可以use来自范围之外的这些变量,并且它们具有内部值。它们功能强大,可用于任何事物。如果使用它们,也会发生相同的特征。

答案 9 :(得分:2)

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

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

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

  1. 使用事件pub / sub代码定义基类,然后想要提供事件的类可以扩展它以获得功能。
  2. 使用事件发布/子代码定义一个类,然后其他想要提供事件的类可以通过组合使用它,定义自己的方法来包装组合对象,代理方法调用它。
  3. 使用事件发布/子代码定义特征,然后其他想要提供事件的类可以use特征,即导入它,以获得功能。
  4. 每种方法的效果如何?

    #1不能正常工作。直到有一天你意识到你不能扩展基类,因为你已经扩展了其他东西。我不会展示一个这样的例子,因为很明显如何限制使用这样的继承。

    #2&amp; #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的实施。

    差异很大。使用合成时,我们得到了一个很好的解决方案,允许我们按照我们喜欢的方式重用我们的EventEmitter。但是,主要的缺点是我们需要编写和维护许多样板代码,因为对于Observable接口中定义的每个方法,我们需要实现它并编写无聊的样板代码来转发参数在我们组成的EventEmitter对象的相应方法上。使用此示例中的特征可以避免,帮助我们减少样板代码并提高可维护性

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

    但是,在大多数情况下,特性非常引人注目,特别是如果界面有很多方法,这会导致你编写大量的样板。

    *你实际上可以做两件事 - 定义EventEmitter类以防你想要在组合上使用它,并定义EventEmitterTrait特性,使用里面的EventEmitter类实现特点:)

答案 10 :(得分:1)

接口是一个合同,上面写着“这个对象能够做到这一点”,而Trait则让对象能够做到这一点。

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

try reading this article

答案 11 :(得分:1)

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

我们可以在课堂内使用特质,也可以在同一个班级中使用多个特征,并使用关键字&#39;。

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

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

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

答案 12 :(得分:0)

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