PHP Trait 使用指南

809 阅读6分钟

通过更好地组织代码和代码复用来最大程度地减少代码重复是面向对象编程的重要目标。但是在 PHP 中,由于使用单一继承模型的局限性,有些时候要做到这些可能会比较困难。您可能有一些要在多个类中使用的方法,但它们可能不太适合继承层次结构。

诸如 C ++ 和 Python 之类的语言允许我们从多个类继承,这在某种程度上解决了这一问题,而 Ruby 中的 mixin 则允许我们混合一个或多个类的功能而无需使用继承。但是多重继承存在诸如 钻石问题 之类的问题,而 mixin 则能是一个复杂的工作机制。

在本文中我打算探讨一下 Trait,这是一个出现在 PHP 5.4 版本中的新特性,可以用来解决此类问题。Trait 的概念本身对编程而言并非什么新鲜事物,而且在其他语言(例如 Scala 和 Perl)中也已经被使用。它允许我们在不同类层次结构中的各个独立类之间水平地重用代码。

我的官方群点击此处

Trait 长什么样

Trait 与抽象类类似,自身无法实例化。PHP 文档对 Trait 的描述如下:

Trait 是为类似 PHP 的单继承语言而准备的一种代码复用机制。Trait 为了减少单继承语言的限制,使开发人员能够自由地在不同层次结构内独立的类中复用 method。

考虑以下的例子:

<?php
class DbReader extends Mysqli
{
}

class FileReader extends SplFileObject
{
}

当不同类都用到一些相同功能时就会出问题。例如,几个类都需要生成单例,由于 PHP 不支持多继承,那么只能在每个类重复编写代码,或者说将单例功能写在父类中并让这几个类都继承这个父类,这种继承是无意义的。Trait 提供了这类问题的解决方案。

<?php
trait Singleton
{
    private static $instance;

    public static function getInstance() {
        if (!(self::$instance instanceof self)) {
            self::$instance = new self;
        }
        return self::$instance;
    }
}

class DbReader extends ArrayObject
{
    use Singleton;
}

class  FileReader
{
    use Singleton;
}

Singleton Trait 直接实现了单例模式。该 Trait 通过 getInstance() 静态方法创建并返回使用该 Trait 的类的实例(如果实例还没有创建)。

让我们试着用 getInstance() 方法创建这些类的对象:

<?php
$a = DbReader::getInstance();
$b = FileReader::getInstance();
var_dump($a);  //object(DbReader)
var_dump($b);  //object(FileReader)

我们可以看到 $aDbReader 的实例,$bFileReader 的实例,但是它们现在都有了实现单例的能力。 通过 Singleton 这个 Trait,它里边的方法已经被水平注入类中。

Trait 不会对类施加任何额外的影响。在某种程度上,你可以将其看作是编译器辅助的复制和粘贴机制,trait 中的方法会被复制到组成类中。

如果我们只是从具有私有属性 $instance 的父类中将类 DbReader 子类化, 属性不会被显示在通过 ReflectionClass::export() 导出的备份中。有了 trait,你就能实现之!

Class [  class FileReader ] {
  @@ /home/shameer/workplace/php54/index.php 19-22

  - Constants [0] {
  }
  - Static properties [1] {
    Property [ private static $_instance ]
  }
  - Static methods [1] {
    Method [  static public method instance ] {
      @@ /home/shameer/workplace/php54/index.php 6 - 11
    }
  }
  - Properties [0] {
  }
  - Methods [0] {
  }
}


多个 Trait

到目前为止我们在在一个类中只用了一个 trait,但在某些情况下,我们可能需要合并多个 trait 的功能。

<?php
trait Hello
{
    function sayHello() {
        echo "Hello";
    }
}

trait World
{
    function sayWorld() {
        echo "World";
    }
}

class MyWorld
{
    use Hello, World;
}

$world = new MyWorld();
echo $world->sayHello() . " " . $world->sayWorld(); //Hello World

在上边的例子中,我们有两个 trait: HelloWorld。 Trait Hello 输出 “Hello” ,而 trait World 输出 “World”。在类 MyWorld 中我们引入 HelloWorld 两个 trait ,以便 MyWorld 类的对象拥有前边两个 trait 的方法,输出 “Hello World”。


Traits 构成的 Traits

随着应用的增长,我们很可能会有一套可用于不同类的 traits 。PHP 5.4 允许我们拥有由其他 traits 构成的 traits 因此我们可以在所有这些类中只包含一个 traits 而不是多个 traits。 这使我们可以重写前一个例子,如下:

<?php
trait HelloWorld
{
    use Hello, World;
}

class MyWorld
{
    use HelloWorld;
}

$world = new MyWorld();
echo $world->sayHello() . " " . $world->sayWorld(); //Hello World

在此我们创建了一个 trait HelloWorld,使用了 traits Hello 和 World 然后包含在 MyWorld 类里。 由于 HelloWorld trait 具有其他两个 traits 的方法,就如同我们自己在类里包含了两个 traits 。


优先顺序

正如我已经提到的,traits 的工作方式就像他们将方法复制并粘贴到类中一样,并且将它们完全压入到类的定义中。在不同的 traits 或类可能具有相同名字的方法,你可能好奇哪个将在子类的对象中起作用。

优先顺序是:

  1. trait 的方法覆盖从父类继承的方法
  2. 当前类定义的方法覆盖 trait 的方法

在下面的例子中清晰体现:

<?php
trait Hello
{
    function sayHello() {
        return "Hello";
    }

    function sayWorld() {
        return "Trait World";
    }

    function sayHelloWorld() {
        echo $this->sayHello() . " " . $this->sayWorld();
    }

    function sayBaseWorld() {
        echo $this->sayHello() . " " . parent::sayWorld();
    }
}

class Base
{
    function sayWorld(){
        return "Base World";
    }
}

class HelloWorld extends Base
{
    use Hello;
    function sayWorld() {
        return "World";
    }
}

$h =  new HelloWorld();
$h->sayHelloWorld(); // Hello World
$h->sayBaseWorld(); // Hello Base World

我们有一个继承自 Base 类的 HelloWorld 类,这两个类都有一个叫 sayWorld() 的方法但是具有不同的返回值。另外,我们在 HelloWorld 类中包含了 Hello trait 。

我们有两个方法,sayHelloWorld()sayBaseWorld(),前者调用了 sayWorld(),该方法两个类和 trait 中都存在。但在输出中, 我们可以看到被调用的是子类。 如果我们需要从父类中引用该方法,则可以使用 sayBaseWorld() 方法中所示的 parent 关键字来实现。


解决冲突和混淆

当使用多个 traits 时可能会出现不同的 traits 使用了相同的方法名。例如,如果尝试运行以下代码 PHP 因为方法名冲突将抛出一个致命错误:

<?php
trait Game
{
    function play() {
        echo "Playing a game";
    }
}

trait Music
{
    function play() {
        echo "Playing music";
    }
}

class Player
{
    use Game, Music;
}

$player = new Player();
$player->play();

无法自动为你解决此类 trait 冲突。 与之相反的是,你必须使用关键字 insteadof 选择在合成类内使用哪种方法。

<?php
class Player
{
    use Game, Music {
        Music::play insteadof Game;
    }
}

$player = new Player();
$player->play(); //玩音乐

这里我们选择在类中使用 Music trait 的 play() 方法,所以 Player 将播放音乐而不是游戏。

在上面的例子中,从两个 traits 中选择了其中一个而不是另一个。在某些情况下你可能想要保留两者并且避免冲突。可以为 trait 中的方法引入一个新名称作为别名。别名不会重命名方法,但是提供了一个备用名称,可以通过该备用名称进行调用。别名使用关键字 as

<?php
class Player
{
    use Game, Music {
        Game::play as gamePlay;
        Music::play insteadof Game;
    }
}

$player = new Player();
$player->play(); //玩音乐
$player->gamePlay(); //玩游戏

现在任何 Player 类的对象都将拥有方法 gamePlay(),该方法等同于 Game::play()。在此应记录下,即使是混淆之后,我们也已经明确解决了所有冲突。


反射

Reflection API 是 PHP 的强大功能之一,它可以用来分析接口,类和方法的内部结构,并能据此进行反向操作。由于我们在谈论 traits,所以您可能有兴趣了解 Reflection API 对 traits 的支持。四个有关 traits 的方法被添加到 PHP 5.4 中的 ReflectionClass,以便我们来获取有关 traits 的信息。

我们可以 ReflectionClass::getTraits() 用来获取一个类中使用的所有 traits 组成的数组。ReflectionClass::getTraitNames() 方法将简要地返回该类中的 traits 名称数组。ReflectionClass::isTrait() 有效地检查某个类是否使用了 trait。

在上一节中,我们讨论了 traits 可以使用别名来避免因具有相同名称而造成冲突现象。ReflectionClass::getTraitAliases() 将返回别名映射到其原始名称的数组。


其他特性

除了上面提到的以外,还有其他一些特性使 traits 更加有趣。我们知道,在经典继承中,子类无法访问类的私有属性。traits 可以访问组成类的私有属性或方法,反之类也可以访问 traits 的私有属性和方法!下面是一个例子:

<?php
trait Message
{
    function alert() {
        echo $this->message;
    }
}

class Messenger
{
    use Message;
    private $message = "This is a message";
}

$messenger = new Messenger;
$messenger->alert(); //This is a message

当 traits 被插入到由它们组成的类中时,trait 的任何属性或方法都将成为该类的一部分,我们可以像访问任何其他类属性或方法一样访问它们。

Trait 中甚至可以使用抽象方法来强制要求使用类必须实现这些抽象方法。例如:

<?php
trait Message
{
    private $message;

    function alert() {
        $this->define();
        echo $this->message;
    }
    abstract function define();
}

class Messenger
{
    use Message;
    function define() {
        $this->message = "Custom Message";
    }
}

$messenger = new Messenger;
$messenger->alert(); //Custom Message

该例中的 Message Trait 拥有一个抽象方法 define(),这将要求所有使用该 Trait 的类都必须实现该方法。否则,PHP 会报抽象方法未被实现的错误。

与 Scale 语言的 Trait 不同,PHP 的 Trait 可以拥有构造器,但是必须将其声明为 public,如果声明 protectedprivate 将会报错。总之,在 Trait 中使用构造器时应当谨慎,因为经常会导致使用类出现意料之外的冲突。


总结

Trait 是 PHP 5.4 引入的最强大的功能之一,本文讨论了 Trait 的大多数特性。通过 Trait,不同的类可以在水平层面上对代码进行复用,而不必拥有相同的继承结构。与复杂的语义相比,Trait 提供了轻量级的代码复用机制。尽管 Trait 存在一些缺点,但是毫无疑问的说,Trait 可以通过消除重复代码来帮助开发者更好的改进应用程序的设计,保持应用程序的 DRY (Don't Repeat Yourself)。

以上内容希望帮助到大家,很多PHPer在进阶的时候总会遇到一些问题和瓶颈,业务代码写多了没有方向感,不知道该从那里入手去提升,对此我整理了一些资料,包括但不限于:分布式架构、高可扩展、高性能、高并发、服务器性能调优、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql优化、shell脚本、Docker、微服务、Nginx等多个知识点高级进阶干货需要的可以免费分享给大家,需要的可以加入我的官方群点击此处