通过 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();

运行结果如下:

-w604

表明在类中成功调用了 Trait 中定义的方法。

由此可见,我们可以轻松通过 Trait + 类的组合扩展类的功能,在某个类中使用了 Trait 之后,就好像把它的所有代码合并到这个类中一样,可以自由调用,并且同一个 Trait 可以被多个类复用,从而突破 PHP 单继承机制的限制,有效提升代码复用性和扩展性。

可见性

Trait 和类一样,支持属性和方法以及可见性设置(privateprotectedpublic),并且即使是 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();

上述代码的打印结果是:

-w570

所以不同于类继承,这完全是把 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 同名的属性:

-w682

方法重写与优先级

属性如此,那方法呢,如果我们尝试在使用了 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 = '柴油';
    }
}

然后在命令行执行代码,打印结果如下:

-w503

可以看到,动力来源变成 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 中定义,在命令行执行代码,打印结果如下:

-w532

这一次变成了 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 中定义的方法了,比如上述代码的打印结果如下:

-w645

一切看起来都很简单,这里,我们还要引入一个新的问题,之前讨论了类中包含了和 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 中代码出现报错提示:

-w699

所以,此时就不存在同名方法覆盖了,而是直接报冲突错误,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,这样一来,上述代码的打印结果就是:

-w590

如果你仍然想调用其他 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 方法,调用 printTextprintEngine 则都将调用 Engine 定义的 printText 方法。所以对应的打印结果如下:

-w604

Trait 组合

Trait 除了可以被类使用来扩展类功能,还可以组合多个 Trait 构建更复杂的 Trait 实现更强大的功能。比如,我们可以编写一个 Component Trait 来组合上面定义的 PowerEngine 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

关于类功能的水平扩展就简单介绍到这里,下篇教程,我们来探讨类的静态方法、魔术方法。

上一篇: 通过对象组合水平扩展 PHP 类功能

下一篇: PHP 静态属性和静态方法