PHP中的特征 - 任何现实世界的例子/最佳实践?

Max*_*Max 146 php traits

特性是PHP 5.4的最大增加之一.我知道语法并理解特征背后的想法,比如日志,安全性,缓存等常见内容的水平代码重用.

但是,我仍然不知道如何在我的项目中使用特征.

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

Gor*_*don 195

我想有一段时间我必须研究具有Traits的语言才能学习公认的Good/Best实践.我目前对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;
}
Run Code Online (Sandbox Code Playgroud)

然后你做(演示)

$foo = new Foo;
$foo->setLogger(new DemoLogger);
$foo->log('It works', 1);
Run Code Online (Sandbox Code Playgroud)

我想在使用traits时要考虑的重要事项是它们实际上只是被复制到类中的代码片段.例如,当您尝试更改方法的可见性时,这很容易导致冲突,例如

trait T {
    protected function foo() {}
}
class A { 
    public function foo() {}
}
class B extends A
{
    use T;
}
Run Code Online (Sandbox Code Playgroud)

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

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

$a = new A;
echo $a->foo();
Run Code Online (Sandbox Code Playgroud)

将打印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();
Run Code Online (Sandbox Code Playgroud)

工作(演示),但现在这个特性与A密切相关,水平重用的整个想法都失去了.

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

我们可以使用聚合/组合(如本页其他地方所示)来实现相同的结果类,但使用聚合/组合的缺点是我们必须手动将代理/委托方法添加到每个类然后应该能够记录.Traits通过允许我将样板保存在一个位置并在需要时选择性地应用它来很好地解决这个问题.

注意:鉴于特征是PHP中的一个新概念,上面表达的所有观点都可能发生变化.我自己还没有太多时间来评估这个概念.但我希望能给你一些思考的东西已经足够了.

  • 这是一个有趣的用例:使用定义合同的接口,使用特征来满足该合同.好的. (39认同)
  • 我喜欢这种真正的程序员,他们提出了一个真正的工作示例,每个程序都有简短的desc.谢谢 (13认同)
  • @sumanchalki抽象类遵循继承规则.如果您需要一个实现Loggable和Cacheable的类,该怎么办?您需要该类来扩展AbstractLogger,然后需要扩展AbstractCache.但这意味着所有Loggable都是缓存.这是你不想要的耦合.它限制了重用并弄乱了继承图. (12认同)

Nik*_*kiC 86

我个人的观点是,在编写干净的代码时,实际上很少有特征应用.

不使用traits将代码破解到类中,最好通过构造函数或通过setter传递依赖项:

class ClassName {
    protected $logger;

    public function __construct(LoggerInterface $logger) {
        $this->logger = $logger;
    }
    // or
    public function setLogger(LoggerInterface $logger) {
        $this->logger = $logger;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

  • NikiC缺少重点:使用特征不会阻止使用依赖注入.在这种情况下,trait只是让每个实现日志记录的类不必复制setLogger()方法和创建$ logger属性.特质会提供给他们.setLogger()会像示例那样在LoggerInterface上键入提示,这样就可以传入任何类型的记录器.这个想法类似于下面的Gordon的答案(只是看起来他在Logger超类而不是Logger接口上的类型提示). (29认同)
  • @rickchristie当然,你可以做到.但是您需要编辑特征的源代码.因此,您可以为使用它的每个类更改它,而不仅仅是您希望使用不同记录器的特定类.如果你想使用同一个类但有两个不同的记录器怎么办?或者,如果您想在测试时传递模拟记录器?如果使用特征,则可以使用依赖注入. (14认同)
  • *目前我认为特征不是"编译器辅助复制和粘贴".;)*:@Max:这正是设计的特征,所以这是完全正确的.它使它更"可维护",因为只有一个定义,但它基本上只是c&p ...... (9认同)
  • 使用特征,您还可以使用另一个记录器类吗?只需编辑特征,所有使用特征的类都会更新.如我错了请纠正我 (4认同)
  • 我能理解你的观点,我也在考虑这些特质是否值得。我的意思是,在像 Symfony 2 这样的现代框架中,你到处都有依赖注入,这在大多数情况下似乎优于特征。目前我认为特征并不比“编译器辅助复制和粘贴”更多。;) (2认同)
  • @noun:我猜你不知道 nikiC 是谁......提示:检查 github 上 php-src 存储库的贡献者:你不需要滚动太多,他是顶级贡献者之一。据您所知,他甚至编写了特征实现的一部分。看到你如此频繁地使用特征:这是一篇关于[为什么你不应该](https://eliasvo.wordpress.com/2015/06/07/php-traits-if-they-werent-evil)的博客文章-他们会很有趣/)。C++ 支持多重继承,但许多开发人员会告诉你不应该走这条路,是的,特质是存在的,这会让它们好吗?不 (2认同)

小智 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>';
?>
Run Code Online (Sandbox Code Playgroud)

作为结束语,我考虑了配件之类的特性(我可以用它来改变我的数据).类似的方法和属性可以从我的类中删除并放在一个地方,以便于维护,更短和更清晰的代码.


tha*_*smt 5

我对 Traits 感到兴奋,因为它们解决了为 Magento 电子商务平台开发扩展时的一个常见问题。当扩展通过扩展向核心类(比如用户模型)添加功能时,就会出现问题。这是通过指向 Zend 自动加载器(通过 XML 配置文件)使用扩展中的用户模型来完成的,并让新模型扩展核心模型。(示例)但是如果两个扩展覆盖了同一个模型呢?你得到一个“竞争条件”,并且只加载了一个。

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

该系统经常导致错误,并且在安装新扩展时需要检查冲突并编辑扩展。这很痛苦,并且会中断升级过程。

我认为使用 Traits 将是完成相同事情的好方法,而无需这种烦人的模型覆盖“竞争条件”。当然,如果多个 Traits 实现具有相同名称的方法,仍然可能存在冲突,但我想像一个简单的命名空间约定可以在很大程度上解决这个问题。

TL;DR 我认为 Traits 可用于为 Magento 等大型 PHP 软件包创建扩展/模块/插件。