轉載:我所理解的 PHP Trait

_杭城浪子發表於2019-01-01

Trait 是從 PHP 5.4 加入的一種細粒度程式碼複用的語法。以下是官方手冊對 Trait 的描述:

Trait 是為類似 PHP 的單繼承語言而準備的一種程式碼複用機制。Trait 為了減少單繼承語言的限制,使開發人員能夠自由地在不同層次結構內獨立的類中複用 method。Trait 和 Class 組合的語義定義了一種減少複雜性的方式,避免傳統多繼承和 Mixin 類相關典型問題。

Trait 和 Class 相似,但僅僅旨在用細粒度和一致的方式來組合功能。 無法透過 trait 自身來例項化。它為傳統繼承增加了水平特性的組合;也就是說,應用的幾個 Class 之間不需要繼承。

什麼是 Trait ?
其實說通俗一點,就是能把重複的方法拆分到一個檔案,透過 use 引入以達到程式碼複用的目的。

那麼,我們應該怎麼樣去拆分我們的程式碼才是合適的呢?我的看法是這樣的:

Trait,譯作 “特性”、“特徵”、“特點” 。那麼問題就來了:什麼才是特性?

一個銷售公司有很多種產品:電視,電腦與滑鼠墊,卡通手辦等。其中滑鼠墊與卡通手辦是非賣品,只用于贈送。

那麼這裡的 “可賣性” 就是一個特性,非賣品是沒有價格的。我們便可以抽象出 “可賣性” 這個 Trait 來:

trait Sellable
{
    protected $price = 0;

    public function getPrice()
    {
        return $this->price;
    }

    public function setPrice(int $price)
    {
        $this->price = $price;
    }
}
當然我們所有的產品都會有品牌與其它基本屬性,所以我們通常會定義一個產品類:

class Pruduct
{
    protected $brand;
    //...

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

    public function getBrand()
    {
        return $this->brand;
    }

    //...
}

我們的電視與電腦類:

class TV extends Pruduct
{
    use Sellable;
    //...

    public function play()
    {
        echo "一臺 {$this->brand} 電視在播放中...";
    }

    //...
}

class Computer extends Pruduct
{
    use Sellable;

    protected $cores = 8;
    //...

    public function getNumberOfCores()
    {
        return $this->cores;
    }

    //...
}
而滑鼠墊與手辦等禮品是不可賣的:

class Gift extends Pruduct
{
    protected $name;

    function __construct($brand, $name)
    {
        parent::__construct($brand);
        $this->name = $name;
    }

    //...
}

上面的這個例子中,“可賣性” 便是部分商品的一個特性,也可以理解為商品的一個歸類。你也許會說,我也可以再新增一個 Goods 類來完成上面的例子啊,Goods 繼承 Product,再讓所有可賣的商品繼承於 Goods 類,把價格屬性與方法寫到 Goods 裡,同樣可以程式碼複用啊。的確,這沒啥問題。但是你會發現:你有多個需要區別的特性時,由於 PHP 只有單繼承的原因,你不得不組合很多個基類出來,將他們層疊,最終得到的樹狀結構是很複雜的。這也是 Trait 所帶來的優勢:隨意組合,程式碼清晰。

其實還有很多例子,比如可飛行的,那麼把飛行這個特性所具有的屬性(如:高度,距離)與方法(如:起飛,降落)放到一個 trait 就是一個合理的拆分。

Trait 有什麼優勢 ?
trait 有什麼優勢?來看一段程式碼:

class User extends Model
{
    use Authenticate, SoftDeletes, Arrayable, Cacheable;

    ...
}

這個使用者模型類,我們引入了四個特性:註冊與授權、軟刪除、陣列式操作、可快取。

我們看到程式碼的時候一眼便知道當前支援了哪些個特性。再看下面另外一種寫法:

abstract AdvansedUser {
  // ... 實現了 Authenticate, SoftDeletes, Arrayable, Cacheable 的所有方法
}
class User extends AdvansedUser
{
    ...
}

你不得不再去閱讀 AdvansedUser 的程式碼才能理解。你想說沒有可讀性是因為我基類的名稱沒起好?可是,這種各種特性組合的一個基類是根本無法起一個見名知義的名稱的,不信你可以試一下。

就算你真的起了一個見名知義的名稱:AuthenticateCacheableAndArrayableSoftDeletesUser,可是當需求變更,要求在 FooUser(同樣繼承了這個基類) 中去除快取特性,而 User 類保留這個特性,怎麼辦?再建立一個基類麼?

這就是我理解的 Trait:

它不僅僅是可複用程式碼段的集合,它應該是一組描述了某個特性的的屬性與方法的集合。它的優點在於隨意組合,耦合性低,可讀性高。

平常寫程式碼的時候也許怎麼拆分才是大家的痛點,分享以下幾個技巧:

從需求或功能描述拆分,而不是寫了兩段程式碼發現程式碼一樣就提到一起;
拆分時某些屬性也一起帶走,比如上面第一個例子裡的價格,它是“可賣性”必備的屬性;
拆分時如果給 Trait 起名困難時,請認真思考你是否真的拆分對了,因為正確的拆分是很容易描述 “它是一個具有什麼功能的特性” 的;
總之一定要記住:不要為了讓兩段相同的程式碼提到一起這樣簡單粗暴的方式來拆分。

以上是個人見解,歡迎各位討論。​:smile:​

本作品採用《CC 協議》,轉載必須註明作者和本文連結
刻意練習,每日精進

相關文章