通过 Trait 水平扩展 PHP 类功能
基本使用
从 PHP 5.4 开始,引入了一种新的代码复用方式 —— Trait,Trait 其实也是一种通过组合水平扩展类功能的机制,我们在 php_learning/oop
目录下新建一个 trait.php
来存放本篇教程的代码,然后基于 Trait 定义动力源,Trait 结构通过关键字 trait
定义:
<?php
trait Power
{
protected function gas()
{
return '汽油';
}
protected function battery()
{
return '电池';
}
}
Trait 和类相似,支持定义方法和属性,但不是类,不支持定义构造函数,因而不能实例化,只能被其他类使用,要在一个类中使用 Trait,可以通过 use
关键字引入,然后就可以在类方法中直接使用 trait 中定义的方法了:
class Car
{
use Power;
public function drive()
{
echo "动力来源:" . $this->gas() . PHP_EOL;
echo "汽车启动..." . PHP_EOL;
}
}
我们编写一段简单的测试代码:
$car = new Car();
$car->drive();
运行结果如下:
表明在类中成功调用了 Trait 中定义的方法。
由此可见,我们可以轻松通过 Trait + 类的组合扩展类的功能,在某个类中使用了 Trait 之后,就好像把它的所有代码合并到这个类中一样,可以自由调用,并且同一个 Trait 可以被多个类复用,从而突破 PHP 单继承机制的限制,有效提升代码复用性和扩展性。
可见性
Trait 和类一样,支持属性和方法以及可见性设置(private
、protected
、public
),并且即使是 private
级别的方法和属性,依然可以在使用类中调用:
<?php
trait Power
{
protected function gas()
{
return '汽油';
}
public function battery()
{
return '电池';
}
private function water()
{
return '水';
}
}
class Car
{
use Power;
public function drive()
{
echo "动力来源:" . $this->water() . PHP_EOL;
echo "切换动力来源:" . $this->battery() . PHP_EOL;
echo "切换动力来源:" . $this->gas() . PHP_EOL;
echo "汽车启动..." . PHP_EOL;
}
}
$car = new Car();
$car->drive();
上述代码的打印结果是:
所以不同于类继承,这完全是把 Trait 的所有代码组合到使用类,变成了使用类的一部分。从另一个角度来印证,就是 Trait 中定义的属性不能再使用类中重复定义。
我们在 Power
Trait 中定义一个属性 $power
,并重构所有代码如下:
<?php
trait Power
{
protected $power;
protected function gas()
{
$this->power = '汽油';
}
public function battery()
{
$this->power = '电池';
}
private function water()
{
$this->power = '水';
}
}
class Car
{
use Power;
public function drive()
{
// 设置动力来源
$this->gas();
echo "动力来源:" . $this->power . PHP_EOL;
echo "汽车启动..." . PHP_EOL;
}
}
$car = new Car();
$car->drive();
可以看到,我们在 Trait 中可以使用 $this
指向当前 Trait 定义的属性和方法,因为 Trait 最终会被类使用,$this
也就最终对应着被使用类的对象实例。然后我们在使用类 Car
中可以通过 $this->power
调用 Trait 属性,就好像调用自己的属性一样。
如果我们试图在 Car
中调用同名属性,会报错,提示不能定义和 Trait 同名的属性:
方法重写与优先级
属性如此,那方法呢,如果我们尝试在使用了 Trait 的类中定义和 Trait 内同名的方法,会发生什么呢?
在 Car
中定义一个和 Power
同名的方法 gas
:
class Car
{
use Power;
public function drive()
{
// 设置动力来源
$this->gas();
echo "动力来源:" . $this->power . PHP_EOL;
echo "汽车启动..." . PHP_EOL;
}
protected function gas()
{
$this->power = '柴油';
}
}
然后在命令行执行代码,打印结果如下:
可以看到,动力来源变成 Car
中定义的 gas
方法设置的 柴油
,也就是说,Car
中定义的 gas
方法覆盖了 Trait 中定义的 gas
方法!
那如果 Car
还继承自父类 BaseCar
,并且 BaseCar
中也定义了和 Trait 中同名的方法,又会如何呢?
abstract class BaseCar
{
abstract public function drive();
protected function gas()
{
echo "动力来源:柴油" . PHP_EOL;
}
abstract function battery();
}
class Car extends BaseCar
{
use Power;
public function drive()
{
// 设置动力来源
$this->gas();
echo "动力来源:" . $this->power . PHP_EOL;
echo "汽车启动..." . PHP_EOL;
}
}
这一次,我们从 Car
中移除 gas
方法,改为在 BaseCar
中定义,在命令行执行代码,打印结果如下:
这一次变成了 Trait 覆盖了父类中定义的同名方法,并且 Trait 中包含了对抽象方法 battery
的实现,所以无需在 Car
中实现该方法。
综上,我们可以看到,同名方法重写的优先级依次是:使用 Trait 的类 > Trait > 父类。并且 Trait 除了不能实例化和可见性上的差异之外,和类继承有着非常多的相似之处,它是介于类继承和标准对象组合之间的一种存在,就像抽象类是不完全的面向接口编程一样。
使用多个 Trait
前面我们提到,一个 Trait 可以被多个不同的类使用,从而实现类功能的水平扩展,同样,一个类也可以使用多个 Trait,比如我们新增一个 Engine
Trait 表示汽车发动机是三缸还是四缸:
trait Engine
{
protected function three()
{
return '三缸发动机';
}
protected function four()
{
return '四缸发动机';
}
}
然后在 Car
中引入:
<?php
trait Power
{
protected function gas()
{
return '汽油';
}
protected function battery()
{
return '电池';
}
}
trait Engine
{
protected function three()
{
return '三缸发动机';
}
protected function four()
{
return '四缸发动机';
}
}
class Car
{
use Power, Engine;
public function drive()
{
// 设置动力来源
echo "动力来源:" . $this->gas() . PHP_EOL;
echo "发送机:" . $this->four() . PHP_EOL;
echo "汽车启动..." . PHP_EOL;
}
}
$car = new Car();
$car->drive();
引用多个 Trait 通过逗号分隔即可,然后我们就可以在 Car
中调用 Engine
Trait 中定义的方法了,比如上述代码的打印结果如下:
一切看起来都很简单,这里,我们还要引入一个新的问题,之前讨论了类中包含了和 Trait 同名的方法会存在覆盖优先级,如果引入多个 Trait 中包含同名方法会发生什么呢?
我们可以测试下:
<?php
trait Power
{
protected $power;
protected function gas()
{
$this->power = '汽油';
}
protected function battery()
{
$this->power = '电池';
}
public function print()
{
echo "动力来源:" . $this->power . PHP_EOL;
}
}
trait Engine
{
protected $engine;
protected function three()
{
$this->engine = 3;
}
protected function four()
{
$this->engine = 4;
}
public function print()
{
echo "发动机个数:" . $this->engine . PHP_EOL;
}
}
这个时候,会看到 Car
中代码出现报错提示:
所以,此时就不存在同名方法覆盖了,而是直接报冲突错误,PHP 提供了如下方式解决这个问题 —— 指定使用多个 Trait 同名方法中的哪一个来替代其他的,这样会导致其他未选择方法被覆盖:
class Car
{
use Power, Engine {
Engine::printText insteadof Power;
}
public function drive()
{
// 设置动力来源
$this->gas();
$this->four();
$this->printText();
echo "汽车启动..." . PHP_EOL;
}
}
我们通过 insteadof
关键字指定使用 Engine
中定义的 printText
,这样一来,上述代码的打印结果就是:
如果你仍然想调用其他 Trait 中的同名方法,PHP 还提供了别名方案,我们可以通过 as
关键字为同名方法设置不同别名,再通过别名来调用对应方法,不过这种方式还是要先通过 insteadof
解决方法名冲突问题:
class Car
{
use Power, Engine {
Engine::printText insteadof Power;
Power::printText as printPower;
Engine::printText as printEngine;
}
public function drive()
{
// 设置动力来源
$this->gas();
$this->four();
$this->printPower();
$this->printText();
$this->printEngine();
echo "汽车启动..." . PHP_EOL;
}
}
在上述代码中,调用 printPower
等同于调用 Power
定义的 printText
方法,调用 printText
和 printEngine
则都将调用 Engine
定义的 printText
方法。所以对应的打印结果如下:
Trait 组合
Trait 除了可以被类使用来扩展类功能,还可以组合多个 Trait 构建更复杂的 Trait 实现更强大的功能。比如,我们可以编写一个 Component
Trait 来组合上面定义的 Power
和 Engine
Trait:
trait Component
{
use Power, Engine {
Engine::printText insteadof Power;
Power::printText as printPower;
Engine::printText as printEngine;
}
protected function init()
{
$this->gas();
$this->four();
}
}
然后在 Car
中直接使用 Component
就可以了:
class Car
{
use Component;
public function drive()
{
// 初始化系统
$this->init();
$this->printPower();
$this->printEngine();
echo "汽车启动..." . PHP_EOL;
}
}
代码整体看起来会更加简洁和灵活,复用性更好。
在设计 Trait 时,我们可以尽可能让每个 Trait 只完成一个功能(单一职责原则),然后通过 Trait 组合的方式灵活构建完成特定任务功能的 Trait,不管是从复杂系统的模块化角度还是从代码复用性、可维护性、可扩展性角度来说,这都是最佳实践。
Trait 还有一些其他的特性,这里就不一一展开了,你可以阅读官方文档查看明细:https://www.php.net/manual/zh/language.oop5.traits.php。
关于类功能的水平扩展就简单介绍到这里,下篇教程,我们来探讨类的静态方法、魔术方法。
6 Comments
哎,可惜,无法按需引入。。。
Trait 可以做个辅助 水平扩展主体还是要依靠对象组合 设计模式
报错了: Fatal error: A precedence rule was defined for Engine::printText but this method does not exist in 定义方法的时候是print,trait重命名的时候是printText,是不是手滑写错了啊。。。
我这也报错这个, 我把 printText 改成 print 后就可以用了。 可能是写错了吧。
嗯 确实是 源代码里都是
printText
,应该是写到后面改了,但是之前的教程里没有同步更新trait 中定义了protected $power , 在class Car 中定义并不会出错。。这个是版本的问题吗??我的环境:php7.1.32
Note: 我在官方文档找到了提示原因,谢谢学院君: 在 PHP 7.0 之前,在类里定义和 trait 同名的属性,哪怕是完全兼容的也会抛出 E_STRICT(完全兼容的意思:具有相同的访问可见性、初始默认值)。