PHP 核心特性 - Trait(Life)
為什麼 PHP 會引入 Trait ? 我們先來看看軟體開發中的兩種常用程式碼複用模式,繼承和組合。
- 繼承:強調父類與子類的關係,即子類是父類的一個特殊型別;
- 組合:強調整體與區域性的關係,側重的一種需要的關係;
軟體開發中有一條原則,叫做組合優於繼承。這是因為從耦合度來看,繼承要高於組合。繼承關係中,子類與父類保持著高度的依賴關係,加上 PHP 不支援多繼承,為了避免重寫編寫程式碼,很多功能都被統一封裝到父類中。這樣做有兩個壞處:一是隨著繼承的層數和子類的增加,程式碼複雜度不斷增加,大量的方法都將面臨著重寫;二是這些功能對於一些子類來說可能是不必要的,破壞了程式碼的封裝性。
Trait 的提出彌補了 PHP 對組合支援的不足,一個 Trait 就相當於一個模組,不同的 Trait 以組合的方式注入到類中。我們以 Laravel 的控制器為例,來介紹下繼承和組合是如何在具體的場景中使用的。
首先,底層的程式碼應當多使用組合。Laravel 的底層控制器只繼承了一個簡單的控制器Illuminate\Routing\Controller
,結構相對穩定。同時,控制器使用了不同的Trait
來組織程式碼,避免了物件的臃腫,極大程度的保持了架構的靈活性。
<?php namespace App\Http\Controllers; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Routing\Controller as BaseController; class Controller extends BaseController { use AuthorizesRequests, DispatchesJobs, ValidatesRequests; }
而具體的業務邏輯或頂層程式碼應當多使用繼承,這樣能夠大大提高的開發效率
<?php use App\Http\Controllers\Controller; class UserController extends Controller { }
以上就是繼承和組合的簡單介紹。接下來看看Trait
的具體使用。
使用規範
Symfony 編碼規範建議在每個Trait
之後新增Trait
關鍵字。
namespace Symfony\Contracts\Translation; trait TranslatorTrait { }
PSR-12 規範建議在每個Trait
use
語句來宣告,同時Trait
與類的其他成員需要保持一行空行。
class ClassName { use FirstTrait; use SecondTrait; use ThirdTrait; public $a; }
成員
Trait
中可包含屬性、方法 與 抽象方法,這三者的結合既可以複用程式碼,也可以對程式碼的使用作出一些約定,例如 Laravel 中的自動維護slug
欄位
<?php namespace App\Traits; use Illuminate\Support\Str; trait HasSlug { public static function bootSluggable() { static::saving(function ($model) { $model->slug = Str::slug( $model->getAttribute( $model->sluggable() ) ); }); } /** * Slug 欄位 * * @return string */ abstract public function sluggable(): string; }
Trait
中也可以包括靜態屬性和靜態方法,以下是一個單例模式的簡單封裝。
trait Singleton { private static $instance; public static function getInstance() { if (!(self::$instance instanceof self)) { self::$instance = new self; } return self::$instance; } }
每個Trait
中可以包含其他Trait
,進一步提高了程式碼的靈活性
<?php trait Hello { function sayHello() { echo "Hello"; } } trait World { function sayWorld() { echo "World"; } } class MyWorld { use Hello; use World; }
Trait 與類的衝突處理
當存在同名方法時,當前類的方法會覆蓋Trait
中的方法,而Trait
中的方法會覆蓋父類的方法。
<?php // 父類,優先順序最低 class Base { public function sayHello() { echo 'Hello '; } } // Trait 優先順序大於父類 trait SayWorld { public function sayHello() { parent::sayHello(); echo 'World!'; } } // 當前類,優先順序最高 class MyHelloWorld extends Base { use SayWorld; } $o = new MyHelloWorld(); $o->sayHello(); // Hello, World
當存在同名屬性時,類的屬性必須與Trait
的屬性相容(相同的訪問性、相同的初始值),否則會報致命錯誤。
<?php trait Foo { public $same = true; public $different = false; } class Bar { use Foo; public $same = true; // 合法 public $different = true; // fatal error }
Trait 與 Trait 的衝突處理
當一個類包含多個Trait
時,不同Trait
之間可能會存在屬性和方法的衝突。
當存在相同屬性時,屬性必須相容,跟Trait
與類的衝突處理類似
Trait A { public $a = 'foo'; } Trait B { public $a = 'foo'; } class Foo { use A; use B; }
當存在方法衝突時,需要使用insteadof
來手動處理衝突,否則會報致命錯誤
<?php Trait A { public function foo() { return "A foo"; } } Trait B { public function foo() { return "B foo"; } } class Bar { use A, B { B::foo insteadof A; // 用 B 的 foo 方法來代替 A } } $bar = new Bar(); echo $bar->foo(); // B foo
這時候如果想要保留 A 的foo
方法,可以用as
定義別名來進行呼叫。注意,起別名僅僅代表可以用別名來呼叫該方法,仍然需要用insteadof
處理衝突
class Bar { use A, B { B::foo insteadof A; // 用 B 的 foo 方法來代替 A A::foo as aFoo; // A 的 foo 方法用 aFoo 來呼叫 } } $bar = new Bar(); echo $bar->aFoo(); // A foo
as
關鍵字還可以用來更改方法法的訪問控制
<?php Trait A { public function foo() { return "A foo"; } } class Bar { use A { A::foo as private; } } $bar = new Bar(); echo $bar->foo(); // Fatal error: Uncaught Error: Call to private method Bar::foo()
這兩者可以結合起來用,這時候原有方法的訪問控制就不會受到影響
<?php Trait A { public function foo() { return "A foo"; } } class Bar { use A { A::foo as private aFoo; } } $bar = new Bar(); echo $bar->foo(); // A foo,照常呼叫 echo $bar->aFoo(); // 被設定成私有方法,因此報錯。Fatal error: Uncaught Error: Call to private method