PHP的特质–任何实际示例/最佳实践?[关闭]


148

特性已成为PHP 5.4的最大新增功能之一。我了解语法并了解特性背后的思想,例如将水平代码重用于日志,安全性,缓存等常见内容。

但是,我仍然不知道如何在项目中利用特质。

是否有任何已经使用了特征的开源项目?关于如何使用特征构造体系结构的任何好的文章/阅读材料?


8
这是我的看法:关于我在该主题上撰写的有关该主题的博客文章。TL; DR:基本上,我担心虽然它们功能强大并且可以很好地使用,但我们将看到的大多数用途将是完全的反模式,并且所造成的痛苦远远超过了解决的困难……
ircmaxell

1
看一下scala标准库,您会发现许多有用的特征示例。
德米特里

Answers:


89

我个人的观点是,在编写简洁的代码时,实际上很少使用特质。

与其使用特征将代码入侵到类中,不如通过构造函数或setter传递依赖项:

class ClassName {
    protected $logger;

    public function __construct(LoggerInterface $logger) {
        $this->logger = $logger;
    }
    // or
    public function setLogger(LoggerInterface $logger) {
        $this->logger = $logger;
    }
}

我发现比使用特征更好的主要原因是,通过消除与特征的硬耦合,您的代码更加灵活。例如,您现在可以简单地传递另一个记录器类。这使您的代码可重用和可测试。


4
使用特征,您还可以使用其他记录器类,对吗?只需编辑特征,使用该特征的所有类都会更新。如果我错了,请纠正我
rickchristie 2011年

14
@rickchristie当然可以。但是您需要编辑特征的源代码。因此,您需要为每个使用它的类更改它,而不仅仅是要为其使用不同记录器的特定类。如果要使用同一类但使用两个不同的记录器怎么办?还是要在测试时通过模拟记录器?如果使用特征,则不能,如果使用依赖项注入,则不能。
NikiC 2011年

2
我明白你的意思,我也在思考特质是否值得。我的意思是,在像Symfony 2这样的现代框架中,您到处都有依赖项注入,在大多数情况下,这种注入似乎比特征更为优越。目前,我所看到的特征不只是“编译器辅助的复制和粘贴”。;)
最大

11
目前,我所看到的特征不只是“编译器辅助的复制和粘贴”。;):@Max:这正是特质被设计成的特征,因此完全正确。因为只有一个定义,所以它更“可维护”,但这基本上只是c&p ...
ircmaxell 2011年

29
NikiC遗漏了要点:使用特征不会阻止使用依赖注入。在这种情况下,特征将只允许每个实现日志记录的类不必复制setLogger()方法和$ logger属性的创建。该特征将提供它们。setLogger()将像示例中那样在LoggerInterface上键入提示,以便可以传递任何类型的记录器。此想法类似于下面的Gordon回答(只是看起来他在Logger超级类而不是Logger接口上键入提示) )。
伊桑(Ethan)2012年

205

我想人们现在必须研究具有特质的语言一段时间,以学习公认的良好/最佳实践。我目前对Trait的看法是,只应将它们用于必须在共享相同功能的其他类中重复的代码。

Logger特性示例:

interface Logger
{
    public function log($message, $level);    
}

class DemoLogger implements Logger
{
    public function log($message, $level)
    {
        echo "Logged message: $message with level $level", PHP_EOL; 
    }
}

trait Loggable // implements Logger
{
    protected $logger;
    public function setLogger(Logger $logger)
    {
        $this->logger = $logger;
    }
    public function log($message, $level)
    {
        $this->logger->log($message, $level);
    }
}

class Foo implements Logger
{
    use Loggable;
}

然后你做(演示

$foo = new Foo;
$foo->setLogger(new DemoLogger);
$foo->log('It works', 1);

我猜想使用特征时要考虑的重要一点是,它们实际上只是复制到类中的一部分代码。例如,当您尝试更改方法的可见性时,这很容易导致冲突。

trait T {
    protected function foo() {}
}
class A { 
    public function foo() {}
}
class B extends A
{
    use T;
}

以上将导致错误(demo)。同样,任何在trait中声明的方法,也已经在using类中声明的方法,都不会复制到该类中,例如

trait T {
    public function foo() {
    return 1;
}
}
class A { 
    use T;
    public function foo() {
    return 2;
}
}

$a = new A;
echo $a->foo();

将打印2(演示)。您将要避免这些事情,因为它们会使错误很难发现。您还希望避免将事物放入对使用它的类的属性或方法进行操作的特征中,例如

class A
{
    use T;
    protected $prop = 1;
    protected function getProp() {
        return $this->prop;
    }
}

trait T
{
    public function foo()
    {
        return $this->getProp();
    }
}

$a = new A;
echo $a->foo();

作品(演示),但现在特性已与A紧密耦合,并且水平重用的整个概念都消失了。

当您遵循接口隔离原则时您将拥有许多小的类和接口。这使Traits成为您提到的事物(例如,横切关注点)而不是构成对象(在结构意义上)的理想人选。在上面的Logger示例中,特征是完全隔离的。它不依赖于具体类。

我们可以使用 聚合/组合(如本页其他位置所示)来获得相同的结果类,但是使用聚合/组合的缺点是我们必须手动将proxy / delegator方法添加到每个类中,能够登录。特质很好地解决了这一问题,方法是允许我将样板放置在一个位置,然后在需要的地方选择性地应用它。

注意:鉴于trait是PHP中的一个新概念,因此上面表达的所有观点可能会发生变化。我还没有太多时间自己评估这个概念。但我希望能给您一些思考的足够好。


41
那是一个有趣的用例:使用定义契约的接口,使用特征以满足契约。好一个
马克斯

13
我喜欢这种真正的程序员,他们为每个程序员都提供了一个简短的desc实例。Thx
亚瑟·库什曼

1
如果有人改用抽象类怎么办?替换接口和特征,就可以创建一个抽象类。同样,如果接口对于应用程序来说是非常必要的,那么抽象类也可以实现接口并像trait一样定义方法。那么您能解释为什么我们仍然需要特质吗?
Sumanchalki

12
@sumanchalki抽象类遵循继承规则。如果您需要一个实现Loggable和Cacheable的类怎么办?您需要该类来扩展AbstractLogger,然后再扩展AbstractCache。但这意味着所有Loggables都是缓存。那是你不想要的耦合。它限制了重用并弄乱了您的继承图。
Gordon

1
我认为演示链接已失效
Pmpr

19

:)我不喜欢理论化和辩论某些事情应该做什么。在这种情况下,特质。我将向您展示我发现对自己有用的特质,您可以从中学习或忽略。

特性 -他们非常擅长运用策略。简而言之,当您希望以不同方式处理(过滤,排序等)相同数据时,策略设计模式很有用。

例如,您有一个要根据某些条件(品牌,规格等)或以不同方式(价格,标签等)进行筛选的产品列表。您可以创建一个排序特征,其中包含针对不同排序类型(数字,字符串,日期等)的不同功能。然后,不仅可以在产品类(如示例中给出)中使用此特征,还可以在需要类似策略(将数字排序应用于某些数据等)的其他类中使用此特征。

试试吧:

<?php
trait SortStrategy {
    private $sort_field = null;
    private function string_asc($item1, $item2) {
        return strnatcmp($item1[$this->sort_field], $item2[$this->sort_field]);
    }
    private function string_desc($item1, $item2) {
        return strnatcmp($item2[$this->sort_field], $item1[$this->sort_field]);
    }
    private function num_asc($item1, $item2) {
        if ($item1[$this->sort_field] == $item2[$this->sort_field]) return 0;
        return ($item1[$this->sort_field] < $item2[$this->sort_field] ? -1 : 1 );
    }
    private function num_desc($item1, $item2) {
        if ($item1[$this->sort_field] == $item2[$this->sort_field]) return 0;
        return ($item1[$this->sort_field] > $item2[$this->sort_field] ? -1 : 1 );
    }
    private function date_asc($item1, $item2) {
        $date1 = intval(str_replace('-', '', $item1[$this->sort_field]));
        $date2 = intval(str_replace('-', '', $item2[$this->sort_field]));
        if ($date1 == $date2) return 0;
        return ($date1 < $date2 ? -1 : 1 );
    }
    private function date_desc($item1, $item2) {
        $date1 = intval(str_replace('-', '', $item1[$this->sort_field]));
        $date2 = intval(str_replace('-', '', $item2[$this->sort_field]));
        if ($date1 == $date2) return 0;
        return ($date1 > $date2 ? -1 : 1 );
    }
}

class Product {
    public $data = array();

    use SortStrategy;

    public function get() {
        // do something to get the data, for this ex. I just included an array
        $this->data = array(
            101222 => array('label' => 'Awesome product', 'price' => 10.50, 'date_added' => '2012-02-01'),
            101232 => array('label' => 'Not so awesome product', 'price' => 5.20, 'date_added' => '2012-03-20'),
            101241 => array('label' => 'Pretty neat product', 'price' => 9.65, 'date_added' => '2012-04-15'),
            101256 => array('label' => 'Freakishly cool product', 'price' => 12.55, 'date_added' => '2012-01-11'),
            101219 => array('label' => 'Meh product', 'price' => 3.69, 'date_added' => '2012-06-11'),
        );
    }

    public function sort_by($by = 'price', $type = 'asc') {
        if (!preg_match('/^(asc|desc)$/', $type)) $type = 'asc';
        switch ($by) {
            case 'name':
                $this->sort_field = 'label';
                uasort($this->data, array('Product', 'string_'.$type));
            break;
            case 'date':
                $this->sort_field = 'date_added';
                uasort($this->data, array('Product', 'date_'.$type));
            break;
            default:
                $this->sort_field = 'price';
                uasort($this->data, array('Product', 'num_'.$type));
        }
    }
}

$product = new Product();
$product->get();
$product->sort_by('name');
echo '<pre>'.print_r($product->data, true).'</pre>';
?>

作为结束语,我考虑了诸如附件之类的特征(可以用来更改数据)。类似的方法和属性可以从我的课程中删除,并放在一个地方,以便于维护,简化和简洁的代码。


1
尽管这可以使公共接口保持整洁,但内部接口可能因此变得非常复杂,尤其是如果您将此扩展到其他事物,例如颜色。我认为简单的函数或静态方法在这里更好。
塞巴斯蒂安·马赫

我喜欢这个词strategies
兰妮·奥尔利特

4

我对Traits感到很兴奋,因为它们在为Magento电子商务平台开发扩展时解决了一个常见问题。当扩展通过扩展将功能添加到核心类(例如User模型)时,就会出现问题。通过指向Zend自动加载器(通过XML配置文件)使用扩展中的User模型,并让该新模型扩展核心模型,即可完成此操作。(示例)但是如果两个扩展覆盖同一模型怎么办?您得到一个“竞赛条件”,并且只有一个被加载。

现在的解决方案是编辑扩展,以便一个扩展一个链中另一个模型的覆盖类,然后设置扩展配置以正确的顺序加载它们,以便继承链起作用。

该系统经常会导致错误,并且在安装新的扩展程序时,有必要检查冲突并编辑扩展程序。这很痛苦,并且破坏了升级过程。

我认为,如果没有此令人讨厌的模型覆盖“竞赛条件”,使用特质将是完成同一件事的好方法。当然,如果多个Traits实现具有相同名称的方法仍然会发生冲突,但是我想像一个简单的命名空间约定就可以在很大程度上解决此问题。

TL; DR我认为Traits对于为大型PHP软件包(如Magento)创建扩展/模块/插件很有用。


0

您可能对只读对象具有如下特征:

  trait ReadOnly{  
      protected $readonly = false;

      public function setReadonly($value){ $this->readonly = (bool)$value; }
      public function getReadonly($value){ return $this->readonly; }
  }

您可以检测是否使用了该特征并确定是否应该将该对象写入数据库,文件等中。


这样,具有use这种特点的班级就会召集if($this -> getReadonly($value)); 但是,如果您没有use此特征,则会产生错误。因此,这个例子是有缺陷的。
Luceos

好吧,您需要检查该特征是否先在使用中。如果在对象上定义了ReadOnly特性,则可以检查它是否为只读。
Nico


3
为此,您应该声明一个ReadOnly接口
Michael Tsang
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.