PHP Trait 使用指南

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

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

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

个人官方群点击此处
shell

Trait 长什么样

Trait 与抽象类相似,自身没法实例化。PHP 文档对 Trait 的描述以下:编程

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

考虑如下的例子:数组

<?php
class DbReader extends Mysqli
{
}

class FileReader extends SplFileObject
{
}复制代码

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

<?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等多个知识点高级进阶干货须要的能够免费分享给你们 ,须要的能够加入个人官方群 点击此处
相关文章
相关标签/搜索