特征与接口

dat*_*ers 324 php interface traits

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

我已经尝试过寻找一个体面的博客文章或解释何时使用其中一个的文章,但到目前为止我发现的例子看起来非常相似.

那里的任何人都可以就此分享他们的意见/观点吗?

rdl*_*rey 516

公共服务声明:

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


我们先说这个:

面向对象编程(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);
  }
}
Run Code Online (Sandbox Code Playgroud)

然后,在您的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;
  }
}
Run Code Online (Sandbox Code Playgroud)

这种方法效果很好.但也许几周后你决定要使用基于文件的缓存系统而不是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)
Run Code Online (Sandbox Code Playgroud)

为此,您可以像这样定义界面:

interface CacherInterface
{
  public function fetch($key);
  public function store($key, $data);
  public function delete($key);
}
Run Code Online (Sandbox Code Playgroud)

反过来,您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!';
    }
}
Run Code Online (Sandbox Code Playgroud)

一个更具体的例子:假设您FileCacher和您ApcCacher的界面讨论使用相同的方法来确定缓存条目是否陈旧并且应该被删除(显然现实情况并非如此,但请继续使用).您可以编写特征并允许两个类将其用于公共接口要求.

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

  • 我真的在寻找上面提供的快速简单的答案,但我必须说你给出了一个非常深入的答案,这将有助于让其他人更清楚地区分,这是一种荣誉. (67认同)
  • "[C] reating traits,它满足给定类中接口所需的功能,是一个理想的用例".确切地说:+1 (34认同)
  • @rdlowrey关于SO的最佳答案之一.谢谢. (7认同)
  • 可以公平地说,PHP中的特征与其他语言中的混合类似吗? (5认同)
  • @igorpan对于所有意图和目的,我会说PHP的特征实现*与多重继承相同.值得注意的是,如果PHP中的特征指定静态属性,那么使用特征的每个类都将拥有自己的静态属性副本.**更重要的是......**在查询特征时,看看这篇帖子现在如何在SERP上非常高我将在页面顶部添加一个公共服务公告.你应该阅读它. (5认同)
  • +1用于深入解释.我来自红宝石背景,混合使用很多; 只是为了加上我的两分钱,我们使用的一个好的经验法则可以在php中翻译为"不要实现在特征中改变$ this的方法".这可以防止一大堆疯狂的调试会话...... mixin也不应该对它将被混合的类做出任何假设(或者你应该非常清楚并将依赖性降低到最低限度).在这方面,我发现你对traits实现接口的想法很好. (3认同)
  • 出色的答案.真正确定界面的力量以及关于特征的大量信息(和警告).确认了一些我对它们的疑虑 - 它们让我觉得有点滑稽,但我不确定为什么直到我读到这个:D (2认同)
  • 答案很棒,使用特征的例子就是一个例子,但在那里使用它们没有意义,想知道为什么?因为没有! (2认同)

Ale*_*rge 230

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

当一个特征是use'd'时,方法的实现也会出现 - 这不会发生在Interface.

这是最大的不同.

PHP RFC水平重用:

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

  • 特征基本上是**语言辅助的复制和粘贴**. (184认同)
  • 除了特征根本不是接口.接口是可以检查的规范.无法检查特征,因此它们仅是实现.它们与接口完全相反.RFC中的那一行是完全错误的...... (77认同)
  • 这不是一个比喻.这就是屠杀一个词的含义.这就像将盒子描述为具有体积的表面一样. (9认同)
  • 扩展ircmaxell和Shadi的注释:您可以检查对象是否实现了接口(通过instanceof),并且可以确保方法参数通过方法签名中的类型提示实现接口.您无法对特征执行相应的检查. (6认同)
  • @JREAM在实践中,没什么.实际上,还有更多. (2认同)

Tro*_*ord 67

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

PHP手册(强调我的):

特征是一种在单继承语言(如PHP)中重用代码的机制.......它是对传统继承的补充,可以实现行为的横向组合; 也就是说,类成员的应用程序不需要继承.

一个例子:

trait myTrait {
    function foo() { return "Foo!"; }
    function bar() { return "Bar!"; }
}
Run Code Online (Sandbox Code Playgroud)

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

class MyClass extends SomeBaseClass {
    use myTrait; // Inclusion of the trait myTrait
}
Run Code Online (Sandbox Code Playgroud)

此时,当我创建一个类的实例时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      |
---------------------------------------------------------------------
Run Code Online (Sandbox Code Playgroud)

多态性:

在前面的示例,其中,MyClass 延伸 SomeBaseClass,MyClass 一个实例SomeBaseClass.换句话说,一个数组如SomeBaseClass[] bases可以包含实例MyClass.同样,如果MyClass扩展IBaseInterface,则数组IBaseInterface[] bases可以包含实例MyClass.没有这样的多态结构可用trait- 因为a 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 */ }
}
Run Code Online (Sandbox Code Playgroud)

在上面创建MyClass的实例时,会发生以下情况:

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

结论

  1. 一个Interfacecan不能提供方法体的默认实现trait.
  2. An Interface是一个多态的,继承的构造 - 而a trait则不是.
  3. 多个Interfaces可以在同一个类中使用,因此可以使用多个traits.

  • "一个特征类似于抽象类的C#概念"不,抽象类是一个抽象类; 这个概念存在于PHP和C#中.我会将PHP中的特征与C#中由扩展方法组成的静态类进行比较,删除基于类型的限制,因为特征可以被几乎任何类型使用,这与仅扩展一种类型的扩展方法不同. (4认同)

J. *_*uni 26

我认为traits创建包含可用作多个不同类的方法的方法的类很有用.

例如:

trait ToolKit
{
    public $errors = array();

    public function error($msg)
    {
        $this->errors[] = $msg;
        return false;
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以在使用此特征的任何类中使用此"错误"方法.

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
    }
}
Run Code Online (Sandbox Code Playgroud)

虽然interfaces你只能声明方法签名,但不能声明其函数的代码.此外,要使用界面,您需要遵循层次结构,使用implements.特征不是这种情况.

它完全不同!

  • 忘记"to_integer" - 这只是一个例子.一个例子.一个"你好,世界".一个"example.com". (5认同)
  • 这个工具包特性提供的独立实用程序类不能提供什么好处?你可以使用`$ this-> toolkit = new Toolkit();或者我错过了特质本身的一些好处,而不是`使用Toolkit`. (2认同)

Sup*_*eth 18

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

性状

trait SayWorld {
    public function sayHello() {
        echo 'World!';
    }
}
Run Code Online (Sandbox Code Playgroud)

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

class MyClass{
  use SayWorld;

}

$o = new MyClass();
$o->sayHello();
Run Code Online (Sandbox Code Playgroud)

好吧!

不仅可以在特征中使用任何函数(函数,变量,常量......).你也可以使用多种特质:use SayWorld,AnotherTraits;

接口

  interface SayWorld {
     public function sayHello();
  }

  class MyClass implements SayWorld { 
     public function sayHello() {
        echo 'World!';
     }
}
Run Code Online (Sandbox Code Playgroud)

所以这就是界面与特征的不同之处:你必须在实现的类中重新创建界面中的所有东西.接口没有实现.和接口只能有函数和const,它不能有变量.

我希望这有帮助!


Raj*_*aul 8

Traits只是为了代码重用

接口仅提供要在类中定义的函数的签名,可以根据程序员的判断来使用该函数。从而为我们提供了一组类的原型

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


Jon*_*ske 5

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

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

首先,instanceof操作符不会处理特征(即特征不是真正的对象),因此您不能使用它来查看一个类是否具有某个特征(或查看两个不相关的类是否共享一个特征)。这就是他们所说的水平代码重用结构。

PHP 中现在有一些函数可以让您获得一个类使用的所有特征的列表,但是 trait-inheritance 意味着您需要进行递归检查以可靠地检查某个类是否具有特定的特征(有示例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  
}
Run Code Online (Sandbox Code Playgroud)

这样做意味着您可以使用instanceof来确定特定的 Door 对象是否是 Keyed 的,您知道您将获得一组一致的方法等,并且所有代码都在使用 KeyedTrait 的所有类中的一个地方。