物件導向程式設計構築起現代程式設計,同時也浪費了工程師們最寶貴的資源--時間。
PHP 從 5.3 之後開始陸續加入物件導向的機制,雖然大部份功能都參考(抄襲)自 Java,但這成為 PHP 成為 Modern PHP 的礎石。
對於常見的 Class 操作,如 extends
、interface
之類的幾乎與 Java 一模一樣--除了在多型(Polymorphism)的表現上 PHP 充份發揮了它身為弱型別語言的優勢。
還有另一項功能,是 PHP 相對於 Java 更加方便的功能:trait
trait
通常中文會翻成「特徵」,不過因為易於混淆所以通常不會進行翻譯。
trait
功能並非 PHP 原創,在同樣運行於 JVM 的語言 Scala 亦有所實現(trait
是否為 Scala 原創這就不得而知)
trait
的存在是為了簡化 Class 功能複用的痛點。舉例來說,假設目前有幾個 Class:
class Man
{
public function walk() { // ... }
public function run() { // ... }
}
class Woman
{
public function walk() { // ... }
public function run() { // ... }
}
當兩個 class 都含有類似的內容(property 或 method)時,我們可以用 trait
簡化之。
trait Moveable
{
public function walk() { // ... }
public function run() { // ... }
}
class Man
{
use Moveable;
}
class Woman
{
use Moveable;
}
這樣的型式偶爾會受到批評,大多數的批評者會認為這樣不夠「OOP」。
面對這樣的使用情景,一般的 OOP 倡導者會認為:應該在 Man 及 Woman 類別之上加入一個父類別 Human,並且讓 Man 及 Woman 繼承自 Human。
abstract class Human
{
public function walk() { // ... }
public function run() { // ...}
}
class Man extends Human
{
}
class Woman extends Human
{
}
甚至在 Human 上面 implements CanMove 這樣的 interface。
然而,這樣的做法在相關程度夠高的情況下是很適用的(例如 Man, Woman 都是 Human,一目瞭然),但是在兩個類別功能與具體形象相差甚遠的情況下,這樣的繼承關係就會變得薄弱,而且可能會變得「為了繼承而繼承」這樣荒唐的情況。
trait
幾乎是為了 method 而生,對於 property
的可用性並不高。
trait CountAge
{
protected $age;
public function getAge(): int { return $this->age; }
public function setAge(int $age): void { $this->age = $age; }
}
class Child
{
use CountAge;
protected $age = 10;
}
在上述的例子中,因為 CountAge 與 Child 中都存在 $age
這個 property,此時便會產生 PHP Fatal error: Child and CountAge define the same property ($age) in the composition of Child. However, the definition differs and is considered incompatible.
不像 method,property 是無法被 Override 的。
trait
被視為「直接 include」進 class 的一部份,所以在 trait 中所定義的內容均會複製進 class。
註:此處為了簡化說明才這樣寫,事實上 trait 的實作還有其它的細節,但已超出本篇範圍就先不提。
也就是說,任何在 trait
中所定義的 method 在使用這個 trait 的 class 都可以使用:
trait CheckAdult
{
private function getAge(): int
{
return $this->age;
}
public function isAdult(): bool
{
return $this->getAge() >= 18;
}
}
class Human
{
use CheckAdult;
protected $age = 18;
public function canAccessPornHub(): bool
{
return $this->isAdult();
}
public function canAccessGayTube(): bool
{
public $this->getAge() >= 18;
}
}
只要 use CheckAdult
,就可以使用 isAdult()
與 getAge()
兩個 method。雖然這讓開發上變得自由,但同時也容易埋下程式碼管理上的禍根。
trait 之間不可以具有相同名稱的 method,否則會丟出 Fatal Error。
trait USD
{
public function getBalance() { // ... }
}
trait TWD
{
public function getBalance() { // ... }
}
class Wallet
{
use USD;
use TWD;
}
上述程式會直接 Fatal Error,因為在 USD 及 TWD 的 trait 中同時存在 getBalance()
這個 method。
承上一段,因為 trait 中定義的 method 是該 class 之間共有的,所以就算我將函式設為 private
也同樣會出現衝突。
trait USD
{
private function convert(string $to) { // ... }
public function getUSDBalance(): int { return $this->convert('USD'); }
}
trait TWD
{
private function convert(string $to) { // ... }
public function getTWDBalance(): int { return $this->convert('TWD'); }
}
class Wallet
{
use USD;
use TWD;
}
上述程式依然會丟出 Fatal Error,而這是我認為 trait 最不合理的部份,我認為應該要加入某些關鍵字(例如 inner
或 inline
),表明某個 method 僅在此 trait 中才能夠被使用。
我認為這是 PHP 在引入 trait
這項功能時,考慮不夠周全導致它的實作可能存在一些缺陷。
trait
在大部份時候是個好用的小工具,但是它的缺點也很明顯:過度的或錯誤的使用很容易讓 Debug 的複雜程度變高(因程式間的藕合度可能會提高)。
大部份情況下 trait
可以配合 interface 一起使用,讓類似的功能可以被輕鬆實現。