Trait [treɪt] 翻译过来是 “特性”、”特点” 、”特质”,是一种在 PHP 中复用代码的形式。
Trait 是为类似 PHP 的单继承语言而准备的一种代码复用机制。Trait 为了减少单继承语言的限制,使开发人员能够自由地在不同层次结构内独立的类中复用 method。 Trait 和 Class 组合的语义定义了一种减少复杂性的方式,避免传统多继承和 Mixin 类相关典型问题。
提出
为什么 PHP 会引入 Trait ? 我们先来看看软件开发中的两种常用代码复用模式,继承和组合。
- 继承:强调 父类与子类 的关系,即子类是父类的一个特殊类型;
- 组合:强调 整体与局部 的关系,侧重的一种需要的关系;
软件开发中有一条原则,叫做组合优于继承。这是因为从耦合度来看,继承要高于组合。继承关系中,子类与父类保持着高度的依赖关系,加上 PHP 不支持多继承,为了避免重写编写代码,很多功能都被统一封装到父类中。这样做有两个坏处:一是随着继承的层数和子类的增加,代码复杂度不断增加,大量的方法都将面临着重写;二是这些功能对于一些子类来说可能是不必要的,破坏了代码的封装性。
Trait 的提出弥补了 PHP 对组合支持的不足,一个 Trait 就相当于一个模块,不同的 Trait 以组合的方式注入到类中。我们以 Laravel 的控制器为例,来介绍下继承和组合是如何在具体的场景中使用的。
首先,底层的代码应当多使用组合。Yii 的底层Model只继承了一个简单的 ActiveRecord ,结构相对稳定。同时,Model 中使用了 Trait
来组织代码,避免了对象的臃肿,极大程度的保持了架构的灵活性。
1 2 3 4 5 6 7 8 9 10 11 12
| <?php
namespace common\models\base;
use Yii; use yii\db\ActiveRecord; use common\lib\traits\HelperModel;
class Base extends ActiveRecord { use HelperModel; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| <?php
namespace common\lib\traits;
trait HelperModel {
public function showErrors() { $errors = ''; foreach ($this->errors as $v) { foreach ($v as $v2) { $errors .= $v2 . "\n"; } } return $errors; }
public function showCreatedAt() { return date('Y-m-d H:i:s', $this->created_at); }
public function showUpdatedAt() { return date('Y-m-d H:i:s', $this->updated_at); }
public function showDateTime($time) { return $time ? date('Y-m-d H:i:s', $time) : 0; }
public function showShortDateTime($time) { return $time ? date('Y-m-d', $time) : 0; } }
|
而具体的业务逻辑或顶层代码应当多使用继承,这样能够大大提高的开发效率
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?php use common\models\base;
class User extends Base { public function fields() { $fields = parent::fields(); return array_merge(parent::fields(), [ 'created_at' => function (self $model) { return $model->showDateTime($model->created_at); }, ]); } }
|
以上就是继承和组合的简单介绍。接下来看看 Trait
的具体使用。
使用
规范
Symfony 编码规范建议在每个 Trait
之后添加 Trait
关键字。
1 2 3 4 5
| namespace Symfony\Contracts\Translation;
trait TranslatorTrait {
}
|
PSR-12 规范建议在每个 Trait
使用一个 use
语句来声明,同时 Trait
与类的其他成员需要保持一行空行。
1 2 3 4 5 6 7 8
| class ClassName { use FirstTrait; use SecondTrait; use ThirdTrait;
public $a; }
|
成员
Trait
中可包含属性、方法 与 抽象方法,这三者的结合既可以复用代码,也可以对代码的使用作出一些约定,例如 Yii 中的自动维护 fullname
字段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <?php
namespace common\lib\traits;
trait UserTrait { public function fields() { $fields = parent::fields(); return array_merge($fields, [ 'fullname' => function (self $model) { return $model->fullname; } ]); }
abstract public function getFullname(): string;
}
|
Trait
中也可以包括静态属性和静态方法,以下是一个单例模式的简单封装。
1 2 3 4 5 6 7 8 9 10 11
| trait Singleton { private static $instance;
public static function getInstance() { if (!(self::$instance instanceof self)) { self::$instance = new self; } return self::$instance; } }
|
每个 Trait
中可以包含其他 Trait
,进一步提高了代码的灵活性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <?php trait Hello { function sayHello() { echo "Hello"; } }
trait World { function sayWorld() { echo "World"; } }
class MyWorld { use Hello; use World; }
|
Trait 与类的冲突处理
当存在同名方法时,当前类的方法会覆盖 Trait
中的方法,而 Trait
中的方法会覆盖父类的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <?php
class Base { public function sayHello() { echo 'Hello '; } }
trait SayWorld { public function sayHello() { parent::sayHello(); echo 'World!'; } }
class MyHelloWorld extends Base { use SayWorld; }
$o = new MyHelloWorld(); $o->sayHello();
|
当存在同名属性时,类的属性必须与 Trait
的属性兼容(相同的访问性、相同的初始值),否则会报致命错误。
1 2 3 4 5 6 7 8 9 10 11
| <?php trait Foo { public $same = true; public $different = false; }
class Bar { use Foo; public $same = true; public $different = true; }
|
Trait 与 Trait 的冲突处理
当一个类包含多个 Trait
时,不同 Trait
之间可能会存在属性和方法的冲突。
当存在相同属性时,属性必须兼容,跟 Trait
与类的冲突处理类似
1 2 3 4 5 6 7 8 9 10 11
| Trait A { public $a = 'foo'; } Trait B { public $a = 'foo'; } class Foo { use A; use B; }
|
当存在方法冲突时,需要使用 insteadof
来手动处理冲突,否则会报致命错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <?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; } }
$bar = new Bar(); echo $bar->foo();
|
这时候如果想要保留 A 的 foo
方法,可以用 as
定义别名来进行调用。注意,起别名仅仅代表可以用别名来调用该方法,仍然需要用 insteadof
处理冲突
1 2 3 4 5 6 7 8 9
| class Bar { use A, B { B::foo insteadof A; A::foo as aFoo; } }
$bar = new Bar(); echo $bar->aFoo();
|
as
关键字还可以用来更改方法法的访问控制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <?php
Trait A { public function foo() { return "A foo"; } }
class Bar { use A { A::foo as private; } }
$bar = new Bar(); echo $bar->foo();
|
这两者可以结合起来用,这时候原有方法的访问控制就不会受到影响
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <?php
Trait A { public function foo() { return "A foo"; } }
class Bar { use A { A::foo as private aFoo; } }
$bar = new Bar(); echo $bar->foo(); echo $bar->aFoo();
|
简单说一下 Trait 在底层的运行原理:PHP 解释器在编译代码时会把 Trait 部分代码复制粘贴到类的定义体中,但是不会处理这个操作引入的不兼容问题,他不仅降低了代码的耦合性,还提升了代码的可读性。依我看来,他不光是某种特性的集合,更像是将某个功能细化了的代码块。