天天看點

掌握 PHP Trait 的概念和用法

1. Trait 是什麼

Trait [treɪt] 翻譯過來是 "特性"、"特點" 、"特質",是一種在 PHP 中複用代碼的形式。

我們看下官方的解釋:

Trait 是為類似 PHP 的單繼承語言而準備的一種代碼複用機制。Trait 為了減少單繼承語言的限制,使開發人員能夠自由地在不同層次結構内獨立的類中複用 method。

Trait 和 Class 組合的語義定義了一種減少複雜性的方式,避免傳統多繼承和 Mixin 類相關典型問題。

上述說明可以提取出幾個關鍵詞:代碼複用、單繼承、減少複雜性。

說到單繼承,不得不提到另外一個特性:多态。多态和繼承是軟體開發中常用的代碼複用方式,但是繼承的方式雖然也能解決問題,但其思路違背了面向對象的原則,顯得很粗暴;多态方式也可行,但不符合軟體開發中的 DRY ( Don't repeat yourself ) 原則,增加了維護成本。

此時此刻,Trait 以一種全新的繼承方式出現了,它既解決了前文叙述的兩種繼承方式的弊端,也相對優雅的實作了代碼的複用。

簡單說一下 Trait 在底層的運作原理:PHP 解釋器在編譯代碼時會把 Trait 部分代碼複制粘貼到類的定義體中,但是不會處理這個操作引入的不相容問題。(是不是很厲害)

2. 定義和使用 Trait

接下來我還會沿用第二節課中的例子,但這次不是買手機,而是自己做手機。假設我們是一家比較特殊的手機廠商,為什麼特殊呢,因為我們什麼品牌的手機都做(就當是山寨工廠吧),廢話不多說,我來說一下制作的規則:

假設我們掌握了三種手機的制作工藝,他們分别是:小米 Note3、三星 Galaxy S8 和 iPhone X,他們各有各的特點(相對于其他兩種品牌獨一無二)還有相同的特點(面部識别),為友善大家了解,我畫了一張圖(看不清圖檔的同學請點選圖檔檢視大圖):

掌握 PHP Trait 的概念和用法

簡單解釋一下:衆所周知,這三部手機都有面部識别的功能(上圖左側部分),這屬于相同點,其實他們的相同點不止這些(比如都可以打電話、發短信等),而了解他們的同學也都知道,其中任何一部手機都有相對于其他手機沒有的功能(上圖右側部分)。

下面我将用代碼的形式講解,當把這些手機抽象為 PHP 類的概念時,如何利用 PHP Trait 更好更優雅的實作這個圖中的功能。

首先,我需要把『面部識别』這三個手機都有的功能抽象為一個 Trait,請看代碼示例:

// 小米 Note3 三星 S8 iPhone X
// 共同擁有的面部識别功能
trait Faceable {
    protected $face_id = 0;
    // 就當我是擷取面部資訊的功能
    public function getFace()
    {
        //...
        return $this->face_id;
    }
    // 就當我是設定面部資訊的功能
    public function setFace(string $face_id)
    {
        //...
        $this->face_id = $face_id;
    }
}      

定義一個 Trait 是不是很簡單,除特殊關鍵字以外,内部構造其實和 PHP 普通的類沒啥差別。這裡需要注意的是,Trait 的命名規範最好是以able 結尾,這樣友善我們自己識别和了解。其次,建議每個檔案隻定義一種性狀,這是良好的實踐。

接下來就是定義這三部手機的類了,需要注意的是,我在實作這三部手機中特有的功能外,同時引用了我剛才定義的面部識别 Trait。

// 小米 Note3
class MiNote3 {
    // 引入面部識别 Trait
    use Faceable;
    // 獨有的 MIUI
    protected $miui;
    // 初始化MIUI
    public function __construct($miui)
    {
        $this->miui = $miui;
        $this->bootUI();
    }

    private function bootUI()
    {
        return $this->miui;
    }

    //...
}      

注:小米的 MIUI 可以說是安卓陣營的佼佼者,用來當做其獨特的特性不為過。

// 三星 GalaxyS8
class SamsangS8 {
    // 引入面部識别 Trait
    use Faceable;
    // 獨有的 Bixby
    protected $bixby;
    
    public function __construct()
    {
        $this->sayHello();
    }

    private function sayHello()
    {
        echo "Hi I am Bixby!";
    }

    //...
}      

注:Bixby 是三星近期釋出的語音助手,雖然還在内測中,我有幸提前體驗了一把,感覺各個方面都碾壓蘋果的 Siri 和其他語音助手。是以把這個 Bixby 的功能作為一個三星S8獨有的特性,抽象出來一個類。

// iPhoneX
class iPhoneX {
    // 引入面部識别 Trait
    use Faceable;
    // 獨有的 TrueDepth
    protected $true_depth;

    public function __construct()
    {
        $this->openCamera();
    }

    private function openCamera()
    {
        return $this->true_depth;
    }
    //...
}      

注:iPhoneX 最好玩的特點莫過于前置的深感攝像頭了,可以把表情動作賦給 emoji,這個功能就可以作為這個手機獨有的特色抽象出來一個類。

通過以上示例,我們就可以把『面部識别』這三部手機都擁有的特性很輕松的組裝到這三部手機中,使他們都具有完整性。用代碼來說呢,就是每個類都使用了一個公共的 PHP Trait,在不改變自身的前提下,擁有了額外的屬性。

3. Trait 的好處

有的人看完上述示例可能會有這樣的疑問:與其像代碼裡這麼寫,我不如建立一個名為 Mobile 的類,在這個類裡完成面部識别的功能,然後讓三個手機的類直接繼承 Mobile 類不就行了嗎?比如:

// 手機公共類
class Mobile {
    protected $message;
    //...
    public function __construct($message)
    {
        $this->message = $message;
    }

    public function getMessage()
    {
        return $this->message;
    }

    // 實作了開關機 鬧鐘 音樂播放等功能...
    # code...
}      

然後這樣引用

// 小米Note3
class MiNote3 extends Mobile {
     // ...
}

// GalaxyS8
class SamsangS8 extends Mobile {
    // ...
}      

答案是可以的,但是你有沒有想過,除了『面部識别』,是不是還有其他的特性我沒有列出來?比如三星和蘋果都有的超大分辨率,小米沒有;小米和三星都是安卓系統,而蘋果不是。這樣的關系,你如果都寫在一個類裡,由于 PHP 隻有單繼承的原因,會使你的 Mobile 類顯得異常臃腫,難以解讀。

// 小米Note3
class MiNote3 {
    use Faceable,Androidable;
    // ...
}

// GalaxyS8
class SamsangS8 {
    use Faceable,Androidable,HDisplayable;
    // ...
}

// iPhoneX
class iPhoneX {
    use Faceable,HDisplayable;
    // ...
}