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等多個知識點高級進階乾貨須要的能夠免費分享給你們 ,須要的能夠加入個人官方羣 點擊此處
相關文章
相關標籤/搜索