如何使用Trait?
Trait是部分class的實現 ,讓我們能將部分class注入到其他class。
trait有兩個功能 :
- 提供如interface的合約。
- 提供如class的實作。
所以trait是一個看起來像interface,但用起來像class的東西。
Version
PHP 5.4
為什麼要使用Trait?
假設現在我們有兩個class : RetailStore class與Car class,我們希望這兩個class都能提供log功能,紀錄class的內部運作。
我們分別想到了3個方法實現log功能 :
- 使用繼承
- 使用多型 (interface)
- 使用trait
使用繼承
| 1 | class Log | 
| 1 | class RetailStore extends Log | 
| 1 | class Car extends Log | 
這種寫法強迫RetailStore與Car去繼承Log,繼承後也會連Log的startLog()與stopLog()一起繼承。
以功能面來說,這樣是可行的,但問題繼承並不是這樣用的。
解釋繼承最典型的例子就是國中學生物的界門綱目科屬種 ,舉例來說,人類屬於 : 
- 界 : 動物界 Animalia
- 門 : 脊索動物門 Chordata
- 綱 : 哺乳綱 Mammalia
- 目 : 靈長目 Primates
- 科 : 人科 Hominidae
- 屬 : 人屬 Homo
- 種 : 智人 H. sapiens
下一層的分類都是上一層的特化,也繼承了上一層分類的所有特性。
也就是智人包含了哺乳綱 的所有特性,所以智人也算是一種哺乳類。
但因為智人還有其他的特性,是哺乳綱所沒有的,所以又再被分類成靈長目。 
但RetailStore class在理解上與Log class不相關,Car class在理解上也與Log class不相關,也就是說RetailStore根本不算是一種Log類,而Car也根本不算是一種Log類,在這裡使用繼承完全是因為Log功能硬兜出來的,而不是因為class天生的特性加以繼承所分類。換句話說,Log class根本不適合當RetailStore class與Log class的祖先。
這是一個典型誤用繼承的例子,並不是一個好的方法。
使用多型
| 1 | interface ILog | 
| 1 | class RetailStore implements ILog | 
| 1 | class Car implements ILog | 
既然繼承不能用,那我們改用多型吧,建立一個ILog interface,定義了startLog()與stopLog()兩個函式,基於之前強迫繼承的錯誤,我們只要求RetailStore class與Car class實踐ILog interface。
使用interface方式比用繼承好多了,但還不是最好,由於RetailStore class與Car class都要實踐ILog interface,這導致了startLog()與stopLog()演算法在RetailStore class與Car class都要實作,也就是說,將來萬一start()與stop()演算法有bug,兩個class都要去修改code,這違反了DRY原則 (Don’t Repeat Yourself)原則,因為相同的code寫兩分,將來維護會很麻煩。
目前看來物件導向兩大絕招繼承與多型 已經陣亡了。
Trait就是為了解決這個問題所產生的,trait允許我們寫一個部分class的TLog trait,然後將Log trait注入到RetailStore class與Car class。
使用Trait
| 1 | trait TLog | 
| 1 | class RetailStore | 
| 1 | class Car | 
既然多型還不夠好,會造成code重複的問題,假如我們能把實踐的部分也寫在interface就好了。trait就是允許我們直接將實踐寫在裡面,避免code重複。
建立一個TLog trait,定義了startLog()與stopLog()兩個函式,直接將startLog()與stopLog()演算法寫在trait的函式內。
既然trait已經將startLog()與stopLog()都寫好了,其他class就不用implements了,PHP使用了use關鍵字,讓我們將trait的函式直接注入到RetailStore class與Car class。也就是說,雖然RetailStore class與Car class都沒有實踐startLog()與stopLog(),透過use住入TLog trait之後,就相當於有了startLog()與stopLog()函式了。 
無論以上3種寫法,對於使用RetailStore class與Car class來說都一樣。1
2
3
4
5
6
7$store = new RetailStore();
$store->startLog();
$store->stopLog();
$car = new Car();
$car->startLog();
$car->stopLog();
繼承的方式雖然可行,但很暴力也不合理;interface方式雖然也可行,但不符合DRY原則,將來不好維護;只有使用trait,一個介於繼承與interface中間的方法,巧妙的融合class與interface的優點,對於class間要加入毫不相關的功能提供了另外一種不錯的解決方式。
對於PHP來說,在使用use關鍵字時,PHP只是將trait的所有變數與函式「複製」進class內,讓class馬上擁有trait的所有功能。
Trait的作用域
因為trait在實作上是使用複製, 所以原本在trait內宣告的public、protected、private變數與函式都會複製到class內,也就是說,class內被trait加進來變數與函式的scope將與原trait完全一樣。
比較一下繼承的extends,當使用繼承時,只有public與protected的變數與函式會被繼承下來,private函式將不會被繼承下來。 
| 1 | trait PrivateTrait | 
第1行1
2
3
4
5
6
7trait PrivateTrait 
{
    private privateFunc() 
    { 
        echo "This is private"; 
    }
}
定義了PrivateTrait,並宣告的一個private函式。
第9行1
2
3
4
5
6
7
8
9
10class PrivateClass 
{
    use privateTrait;
    // This is allowed.
    public publicFunc() 
    {
        $this->privateFunc();
    }
}
publicFunc()函式呼叫了在PrivateTrait定義的privateFunc(),雖然privateFunc()是private,但因為沒用繼承,所以在class內可以使用。
20行1
2
3
4
5
6
7
8
9class ExtendingClass extends PrivateClass 
{
    // This is NOT allowed - privateFunc() is only available
    // to the class which directly utilizes it.
    public someFunc() 
    {
        $this->privateFunc();
    }
}
ExtendingClass 是繼承自PrivateClass,但在someFunc()函式裡呼叫privateFunc()是錯誤的,因為privateFunc()在PrivateTrait定義是private,而private無法被繼承,所以在ExtendingClass是看不到privateFunc()的。
Insteadof與As關鍵字
由於PHP使用複製來實作use,所以有可能出現trait所定義的變數與函式已經在其他class已經被定義,PHP提出了insteadof與as關鍵字來解決。
| 1 | trait A | 
第1行1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25trait A 
{
    function someFunc() 
    {
        ...
    }
    function otherFunc() 
    {
        ...
    }
}
trait B 
{
    function someFunc() 
    {
        ...
    }
    function otherFunc() 
    {
        ...
    }
}
兩個trait所定義的函式完全相同。
25行1
2
3
4
5
6
7class MyClass 
{
    use A, B {
        A::someFunc insteadof B;
        B::otherFunc as differentFunc;
    }
}
MyClass同時使用了A trait與B trait,但因為這兩個trait的函式完全相同,所以造成了函式名稱衝突。
use關鍵字同時加上了A與B兩個trait,後面加上大括號描述該怎麼處理函式名稱衝突。
insteadof關鍵字告訴PHP我們要使用A trait的someFunc()而不要使用B trait的someFunc()。
as關鍵字可以替trait的變數名稱與函式名稱取別名。
以上面的例子,因為沒有描述B::someFunc(),所以就被忽略而消失不見了。
若要使用B::someFunc()可以自行加上as另外取別名。
繼承 vs. 多型 vs. Trait
回到一個更基本的問題,何時該用繼承?何時該用多型?何時該用trait?
繼承
想要重複使用既有程式功能。缺點是耦合度高,當你繼承了某個class時,已經綁死了某個class,而且繼承在編譯時期就已經決定,無法在執行時期改變。
多型
想要解耦合既有程式功能。只要訂出interface,未來有新的功能只要實作interface即可。由於多型特性,可以在執行時期動態改變物件。優點是耦合度低,耦合只到interface層級,不會被特定class綁死。
Trait
想要共享相同程式功能。有一句話說 : 
Inheritance is for extending logic, trait is for sharing behavior.
也就是所謂的Vertial Inheritance and Horizontal Reuse。繼承看起來像是垂直的結構,而trait看起來像是水平的結構。
實務上多型可以取代繼承,但trait卻無法取代多型與繼承,只要需要共享相同的功能,就要多多使用trait。
Good Practice
一個檔案只使用一個Trait
就語法上,PHP允許一個檔案內包含多個trait :1
2
3
4
5
6
7
8
9
10
11
12
13
14
15trait TFoo 
{
    public function getFoo() 
    {
        ...
    }
}
trait TBar 
{
    public function getBar() 
    {
        ...
    }
}
但實務上,基於一個class一個檔案one class per file原則,一個檔案也應該只包含一個trait。
Namespace的Use與Trait的Use
雖然namespace與trait都使用use關鍵字,但彼此並不會混淆。
| 1 | namespace MyNamespace\SubNamespace; | 
- 當use用在namespace時,會寫在class外面。
- 當use用在trait時,會寫在class裡面。
與其他程式語言比較
Ruby
| 1 | module MyTrait | 
宣告trait :
- Ruby沒提供trait關鍵字,要用module模擬。
注入trait :
- include關鍵字是instance method
- extend關鍵字是class method(相當於PHP的 static function)。
PHP
| 1 | trait MyTrait | 
宣告trait :
- 使用trait關鍵字。
注入trait :
- 在class內部使用use關鍵字。
Conclusion
- trait可以簡單的想成部分class,允許我們將trait注入到其他的class內,有些應用在繼承與多型都不好用時,可以考慮用trait。
- insteadof與- as關鍵字可解決trait與class函式與變數名稱衝突的問題。
- 實務上多型可以取代繼承,但trait卻無法取代多型與繼承,只要需要共享相同的功能,就要多多使用trait。
- namespace與trait都使用了use關鍵字,唯用在namespace時會寫在class外面,而用在trait時會寫在class裡面。