PHP中的特征 - 有哪些现实世界的例子/最佳实践?

164

Traits 是PHP 5.4中最大的新增功能之一,它可以实现代码的横向重用,以支持日志记录、安全性、缓存等常见需求。

然而,我仍不知道如何在我的项目中使用traits。

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


8
这是我的观点:这是一篇我写的博客文章,讨论了这个主题。简而言之:虽然Trait(特征)功能强大且可以用于好处,但我们将看到的大多数使用情况都将是完全反模式,并会带来比它们解决的问题更多的痛苦... - ircmaxell
1
看一下Scala标准库,你会发现很多有用的特质示例。 - dmitry
一个被4个用户关闭的帖子怎么会有超过160个赞? - crafter
5个回答

226

我猜想现在需要花费一些时间学习具有特质的语言,以了解被接受的最佳实践。我目前对特质的看法是,你应该只在其他共享相同功能的类中需要重复代码时使用它们。

例如,一个记录器特质:

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);

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

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

上述操作将导致错误 (演示)。同样,任何在 trait 中声明的方法如果在使用类中已经声明,则不会被复制到该类中,例如:
trait T {
    public function foo() {
    return 1;
}
}
class A { 
    use T;
    public function foo() {
    return 2;
}
}

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

将会打印出2 (演示)。这些是你需要避免的事情,因为它们让错误难以发现。你还应该避免将操作类属性或方法的trait放入使用它的类中。

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();

这段代码可以正常工作(演示),但现在这个Trait与A紧密耦合,水平重用的整个概念都丢失了。

当你遵循接口隔离原则时,你会有许多小的类和接口。这使得Trait成为你所提到的事情的理想选择,例如横切关注点,但不适合组合对象(从结构上来说)。在上面的Logger示例中,Trait是完全隔离的。它没有依赖于具体类。

我们可以使用聚合/组合(如本页其他地方所示)来实现相同的结果类,但使用聚合/组合的缺点是我们将不得不手动添加代理/委托方法到每个应该能够记录日志的类中。Traits通过允许我将样板放在一个地方并有选择地应用它解决了这个问题。

注意:鉴于Traits是PHP中的一个新概念,上述所有意见都可能会改变。我还没有太多时间来评估这个概念。但我希望它足以给你一些思考。


44
这是一个有趣的用例:使用定义契约的接口,使用 trait 来满足该契约。不错。 - Max
14
我喜欢这种真正的程序员,他们提供一些具体的工作示例,并为每个示例提供简短的描述。谢谢。 - Arthur Kushman
1
如果有人使用抽象类呢?可以用抽象类替换接口和特质。此外,如果应用程序需要接口,抽象类也可以实现接口并像特质一样定义方法。那么你能解释一下为什么我们仍然需要特质吗? - sumanchalki
12
抽象类遵循继承规则。但如果你需要一个实现Loggable和Cacheable接口的类,那么这个类就需要继承AbstractLogger,而AbstractLogger又需要继承AbstractCache。这意味着所有的Loggables都是Caches,这种耦合不利于代码重用并且会混乱你的继承图。 - Gordon
1
我认为演示链接已经失效。 - Peyman Mohamadpour
显示剩余3条评论

104

我个人的观点是,在编写清晰代码时,traits 的应用实际上非常有限。

与其使用 traits 来将代码“hack”到类中,不如通过构造函数或 setter 方法传递依赖项更好:

class ClassName {
    protected $logger;

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

我认为这样做比使用特征更好的主要原因在于,通过删除对特征的硬编码,您的代码变得更加灵活。例如,现在您可以简单地传递不同的记录器类。这使得您的代码可重用和可测试。


5
使用特质,您可以使用另一个记录器类,对特质进行编辑后,使用该特质的所有类都会更新。如果我说错了,请纠正我。 - rickchristie
18
没问题,你可以这样做。但是你需要编辑特性的源代码。所以你会更改使用它的每个类,而不仅仅是你想要一个不同记录器的特定类。如果你想要使用相同的类但是有两个不同的记录器怎么办?或者在测试时想要传递一个模拟记录器?如果你使用特性就不能这样做,但是如果你使用依赖注入则可以。 - NikiC
3
我能理解你的观点,我也在思考traits是否值得使用。我的意思是,在像Symfony 2这样的现代框架中,你可以在很多地方使用依赖注入,这似乎比traits在大多数情况下更优越。目前,我认为traits不过是“编译器辅助复制和粘贴”。;) - Max
11
目前我认为traits不过是“编译器辅助的复制粘贴”。@Max:那正是traits的设计初衷,所以完全正确。这使得代码更易于“维护”,因为只有一个定义,但实际上就是复制粘贴。 - ircmaxell
31
NikiC误解了重点:使用Trait并不会防止使用依赖注入。在这种情况下,Trait仅仅是让每个实现日志记录的类不必重复编写setLogger()方法和创建$logger属性。Trait将为它们提供此功能。就像示例一样,setLogger()将对LoggerInterface进行类型提示,以便可以传递任何类型的日志记录器。这个想法与下面Gordon的答案类似(只是他看起来对Logger超类而不是Logger接口进行了类型提示)。 - Ethan
显示剩余14条评论

19

:) 我不喜欢推测和辩论关于某事应该怎样做。在这种情况下是特质。我将展示给你特质在哪些方面有用,你可以从中学习,或者忽略它。

特质(Traits) - 它们非常适用于应用策略(strategies)。简而言之,策略设计模式在需要以不同方式处理(过滤、排序等)相同数据时非常有用。

例如,你有一个产品列表,想要根据某些标准(品牌、规格等)进行筛选,或按不同方式(价格、标签等)排序。你可以创建一个排序特质,其中包含不同的函数用于不同类型的排序(数字、字符串、日期等)。然后你不仅可以在产品类中使用这个特质(如示例所示),还可以在其他需要类似策略(对一些数据应用数字排序等)的类中使用。

试一试:

<?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
虽然这样可以保持公共接口的清晰,但内部接口可能会变得非常复杂,特别是如果您将其扩展到其他方面,例如颜色。我认为最好使用简单的函数或静态方法。 - Sebastian Mach
我喜欢术语“策略”。 - Rannie Ollit

5
我对Traits感到兴奋,因为它们解决了在为Magento电子商务平台开发扩展时遇到的常见问题。当扩展通过扩展核心类(比如说用户模型)来添加功能时,就会出现问题。这是通过将Zend自动加载器(通过XML配置文件)指向来自扩展的User模型并让新模型扩展核心模型来完成的。(示例)但是如果两个扩展覆盖了同一个模型呢?你就会得到一个“竞争条件”,只有一个会被加载。
目前的解决方案是编辑扩展,使一个扩展继承另一个的模型覆盖类,并设置扩展配置以正确的顺序加载它们,使继承链正常工作。
这个系统经常会导致错误,安装新扩展时需要检查冲突并编辑扩展。这很麻烦,而且会破坏升级过程。
我认为使用Traits是一种很好的方法,可以在不出现烦人的模型覆盖“竞争条件”的情况下实现相同的功能。尽管如果多个Traits实现具有相同名称的方法仍可能存在冲突,但我想一个简单的命名空间约定就可以解决这个问题。简而言之,我认为Traits对于创建大型PHP软件包(如Magento)的扩展/模块/插件非常有用。

但普通类已经可以实现这一点,你只需要以某种特定的方式使用它们,比如适配器、装饰器、工厂等。 - James

1
您可以为只读对象设置一个特性,例如:
  trait ReadOnly{  
      protected $readonly = false;

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

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

因此,使用这个 trait 的类将调用 if($this -> getReadonly($value));但是如果您没有使用这个 trait,这将会生成一个错误。因此,这个例子是有缺陷的。 - Luceos
首先,您需要检查特征是否正在使用。如果在对象上定义了ReadOnly特征,则可以检查它是否为只读。 - Nico
我在 https://gist.github.com/gooh/4960073 中为这种特性做了一个通用的概念验证。 - Gordon
3
为此,您应该声明一个只读接口。 - Michael Tsang

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接