如何使用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裡面。