ChatGPT解决这个技术问题 Extra ChatGPT

特征与接口

我最近一直在尝试学习 PHP,但我发现自己对特性很感兴趣。我了解水平代码重用的概念,并且不想必然从抽象类继承。我不明白的是:使用特征与接口之间的关键区别是什么?

我已经尝试搜索一篇不错的博客文章或文章,解释何时使用其中一个,但到目前为止我发现的示例似乎非常相似,以至于完全相同。

接口在函数体中没有任何代码。它们实际上没有任何功能体。
尽管我的回答得到了很多人的支持,但我还是希望它记录在案,我通常是 anti-trait/mixin。检查此聊天记录以阅读 how traits often undermine solid OOP practices
我会反驳。在 trait 出现之前和之后使用 PHP 多年,我认为证明它们的价值很容易。只需通读 this practical example,它使“图像模型”也可以像 Imagick 对象一样走路和说话,而不是过去特征出现之前所需的所有臃肿。
特征和界面相似。主要区别在于 Traits 允许您实现方法,而 Interface 不允许。

P
Peter Mortensen

公共服务声明:

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

让我们先这样说:

面向对象编程 (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 类进行了编程以使用接口所需的功能。

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

性状

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

当多个类共享相同的功能(可能由相同的接口决定)时,您应该只使用特征。使用 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 在接口讨论中使用相同的方法来确定缓存条目是否过时并且应该被删除(显然这不是现实生活中的情况,但是去用它)。您可以编写一个特征并允许两个类将其用于公共接口要求。

最后要提醒一句:小心不要过度使用特质。当独特的类实现就足够时,特征通常被用作糟糕设计的拐杖。您应该将特征限制为满足最佳代码设计的接口要求。


我真的在寻找上面提供的快速简单的答案,但我不得不说你给出了一个很好的深入答案,这将有助于让其他人更清楚地区分,荣誉。
“[C]创建满足给定类中接口所需功能的特征是一个理想的用例”。确切地说:+1
可以公平地说 PHP 中的 trait 类似于其他语言中的 mixin 吗?
@igorpan 出于所有意图和目的,我会说 PHP 的 trait 实现与多重继承相同。值得注意的是,如果 PHP 中的 trait 指定了静态属性,那么每个使用该 trait 的类都将拥有自己的静态属性副本。更重要的是......在查询特征时,看到这篇文章现在在 SERP 上的排名非常高,我将在页面顶部添加一个公共服务公告。你应该阅读它。
+1 进行深入解释。我来自红宝石背景,其中混入了很多;只是为了增加我的两分钱,我们使用的一个好的经验法则可以在 php 中翻译为“不要实现在特征中改变 $this 的方法”。这可以防止一大堆疯狂的调试会话...... mixin 也不应该对将要混入的类做出任何假设(或者您应该非常清楚并将依赖关系减少到最低限度)。在这方面,我发现您对实现接口的特征的想法很好。
C
Chris Bornhoft

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

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

这是最大的不同。

Horizontal Reuse for PHP RFC

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


@JREAM 在实践中,什么都没有。实际上,更多。
除了特征根本不是接口。接口是可以检查的规范。无法检查特征,因此它们只是实现。它们与接口完全相反。 RFC 中的那一行是完全错误的……
特征本质上是语言辅助的复制和粘贴。
那不是比喻。那是在扼杀一个词的意思。这就像将一个盒子描述为一个有体积的表面。
扩展 ircmaxell 和 Shadi 的评论:您可以检查对象是否实现接口(通过 instanceof),并且可以确保方法参数通过方法签名中的类型提示实现接口。您无法对特征执行相应的检查。
T
Troy Alford

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

Interface IBase 需要提供一个名为 SomeMethod() 的无参数函数。基类 BaseClass 提供了这种方法的实现——满足需要。 trait myTrait 还提供了一个名为 SomeMethod() 的无参数函数,它优先于 BaseClass 版本 MyClass 类提供了自己的 SomeMethod() 版本 - 它优先于 trait 版本。

结论

接口不能提供方法体的默认实现,而 trait 可以。接口是多态的、继承的构造——而特征不是。多个接口可以在同一个类中使用,多个特征也可以。


“特征类似于抽象类的C#概念”不,抽象类是抽象类;这个概念存在于 PHP 和 C# 中。我会将 PHP 中的 trait 与 C# 中由扩展方法组成的静态类进行比较,去掉基于类型的限制,因为几乎任何类型都可以使用 trait,这与仅扩展一种类型的扩展方法不同。
很好的评论——我同意你的看法。在重新阅读时,这是一个更好的类比。不过,我认为最好还是将其视为 mixin - 当我重新审视我的答案的开头时,我已经更新以反映这一点。感谢您的评论,@BoltClock!
我认为与 c# 扩展方法没有任何关系。扩展方法被添加到单个类类型(当然尊重类层次结构),它们的目的是增强具有附加功能的类型,而不是在多个类上“共享代码”并弄得一团糟。没有可比性!如果需要重用某些东西,通常意味着它应该有自己的空间,例如与需要通用功能的类相关的单独类。实现可能因设计而异,但大致就是这样。特征只是编写糟糕代码的另一种方法。
一个类可以有多个接口?我不确定我是否弄错了您的图表,但是 X 类实现 Y,Z 是有效的。
k
kres0345

我认为 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 接口中,因为没有与(智能)将类转换为整数的基本相似的方法。
忘记“to_integer”——这只是一个插图。一个例子。一个“你好,世界”。一个“example.com”。
这个工具包特性提供了独立实用程序类无法提供的什么好处?您可以使用 $this->toolkit = new Toolkit(); 而不是 use Toolkit 还是我错过了特性本身的一些好处?
@Anthony 在您 Something 的容器中的某处,您执行 if(!$something->do_something('foo')) var_dump($something->errors);
k
kres0345

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

性状

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

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

class MyClass{
  use SayWorld;

}

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

酷对!

不仅是函数,您还可以在 trait 中使用任何东西(函数、变量、常量...)。此外,您可以使用多个特征:use SayWorld, AnotherTraits;

界面

  interface SayWorld {
     public function sayHello();
  }

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

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

我希望这有帮助!


R
Rajesh Paul

特征只是为了代码重用。

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

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


k
kres0345

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

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

首先,instanceof 运算符不适用于特征(即特征不是真实对象),因此您不能使用它来查看类是否具有特定特征(或查看两个其他方面是否不相关类共享一个特征)。这就是他们所说的水平代码重用结构的意思。

现在在 PHP 中有 are 函数可以让您获得一个类使用的所有特征的列表,但是 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  
}

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


该答案的最后一部分当然是@rdlowrey 在他的帖子的“特征”下的最后三段中更详细地说的内容;我只是觉得一个非常简单的骨架代码片段将有助于说明它。
我认为使用特征的最佳 OO 方式是尽可能使用接口。如果有多个子类为该接口实现相同类型的代码并且您不能将该代码移动到它们的(抽象)超类 -> 使用特征实现它
P
Peter Mortensen

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

使用特征是危险的,因为在执行之前没有办法知道它做了什么。

然而,traits 更灵活,因为它们缺乏继承等限制。

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

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


仍然不明白特征与具有默认实现的接口有何不同
@denis631 您可以将 Traits 视为代码片段,将接口视为签名合约。如果它可以提供帮助,您可以将其视为可以包含任何内容的类的非正式部分。让我知道它是否有帮助。
我看到 PHP 特征可以被视为宏,然后在编译时扩展/只是用该键为该代码片段别名。然而,Rust 特征看起来不同(或者我错了)。但是由于它们都具有单词特征,我会假设它们是相同的,意思是相同的概念。 Rust 特征链接:doc.rust-lang.org/rust-by-example/trait.html
T
Thielicious

如果您懂英语并且知道 trait 的含义,那么它就是名字所说的。它是一个无类的方法和属性包,您可以通过键入 use 附加到现有类。

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


g
goat

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

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

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

使用事件发布/订阅代码定义一个基类,然后想要提供事件的类可以扩展它以获得功能。使用事件发布/订阅代码定义一个类,然后其他想要提供事件的类可以通过组合使用它,定义自己的方法来包装组合对象,代理方法调用。使用事件发布/订阅代码定义一个特征,然后其他想要提供事件的类可以使用该特征(也就是导入它)来获得功能。

每个工作的效果如何?

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

请注意,EventEmitterTrait 中的代码与 EventEmitter 类中的代码完全相同,只是 trait 将 triggerEvent() 方法声明为受保护。因此,您需要查看的唯一区别是 Auction 类的实现

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

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

但是,该 trait 在大多数情况下都非常引人注目,特别是如果接口有很多方法,这会导致您编写大量样板文件。

* 你实际上可以同时做这两种 - 定义 EventEmitter 类以防你想组合使用它,并定义 EventEmitterTrait 特征,使用特征内的 EventEmitter 类实现:)


A
Alessandro Martin

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


C
Chirag Prajapati

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

我们可以在类中使用 trait,也可以在同一个类中使用“use 关键字”来使用多个特征。

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

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

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


P
Peter Mortensen

接口是说“这个对象能够做这件事”的契约,而特性是赋予对象做这件事的能力。

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

Try reading this article, What are PHP traits?