iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 4
1
Modern Web

成為 Modern PHPer系列 第 4

Day 04:trait 的使用

物件導向程式設計構築起現代程式設計,同時也浪費了工程師們最寶貴的資源--時間。

前言

PHP 從 5.3 之後開始陸續加入物件導向的機制,雖然大部份功能都參考(抄襲)自 Java,但這成為 PHP 成為 Modern PHP 的礎石。

對於常見的 Class 操作,如 extendsinterface 之類的幾乎與 Java 一模一樣--除了在多型(Polymorphism)的表現上 PHP 充份發揮了它身為弱型別語言的優勢。

還有另一項功能,是 PHP 相對於 Java 更加方便的功能:trait

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,一目瞭然),但是在兩個類別功能與具體形象相差甚遠的情況下,這樣的繼承關係就會變得薄弱,而且可能會變得「為了繼承而繼承」這樣荒唐的情況。

使用時的應注意事項

property 無法被 Override

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 中定義的 method 為共有的

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 的衝突

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 最不合理的部份,我認為應該要加入某些關鍵字(例如 innerinline),表明某個 method 僅在此 trait 中才能夠被使用。

我認為這是 PHP 在引入 trait 這項功能時,考慮不夠周全導致它的實作可能存在一些缺陷。

後記

trait 在大部份時候是個好用的小工具,但是它的缺點也很明顯:過度的或錯誤的使用很容易讓 Debug 的複雜程度變高(因程式間的藕合度可能會提高)。

大部份情況下 trait 可以配合 interface 一起使用,讓類似的功能可以被輕鬆實現。


上一篇
Day 03:PSR-12 概述
下一篇
Day 05:密碼儲存的實踐
系列文
成為 Modern PHPer30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言