Traits

到目前为止,您已经了解到,从类中扩展可以继承代码(属性和方法实现),但它有一个限制,即每次只能从一个类中扩展。另一方面,您可以使用接口从同一个类中实现多个行为,但不能以这种方式继承代码。为了弥补这一缺陷,也就是为了能从多个地方继承代码,我们有了特质。

Traits 是一种允许重用代码的机制,可以同时从多个来源 "继承" 或复制粘贴代码。作为抽象类或接口,特质不能被实例化;它们只是可以从其他类中使用的功能的容器。

如果你还记得,我们在 Person 类中有一些管理 ID 分配的代码。这些代码其实并不属于 Person,而是 ID 系统的一部分,其他实体也可以使用它来识别 ID。从 Person 中提取这一功能的一种方法—​我们并没有说这是最好的方法,但为了让大家看到特质的实际应用,我们选择了这种方法—​就是将其移至 trait 中。

要定义一个 trait,就像定义一个类一样,只需使用关键字 trait 而不是 class。定义它的命名空间,添加所需的 use 语句,声明它的属性和实现它的方法,并将所有内容放在一个遵循相同约定的文件中。在 src/Utils/Unique.php 文件中添加以下代码:

<?php

namespace Bookstore\Utils;

trait Unique {
    private static $lastId = 0;
    protected $id;

    public function setId(int $id) {
        if (empty($id)) {
            $this->id = ++self::$lastId;
        } else {
            $this->id = $id;
            if ($id > self::$lastId) {
                self::$lastId = $id;
            }
        }
    }

    public static function getLastId(): int {
        return self::$lastId;
    }

    public function getId(): int {
        return $this->id;
    }
}

请注意,由于我们将代码存储在不同的文件中,因此命名空间与往常不同。这是一个约定俗成的问题,但你完全可以根据具体情况使用你认为更好的文件结构。在本例中,我们认为该特性并不像客户和书籍那样代表 "业务逻辑",相反,它代表了一种管理 ID 分配的实用工具。

我们包含 Person 中与 ID 相关的所有代码。这包括属性、获取器和构造函数内部的代码。由于该特质不能实例化,所以我们不能添加构造函数。相反,我们添加了一个包含代码的 setId 方法。在构建使用此特质的新实例时,我们可以调用 setId 方法,根据用户发送的参数设置 ID。

Person 类也必须更改。我们必须删除对 ID 的所有引用,并以某种方式定义该类正在使用该特质。为此,我们要使用关键字 use,就像在命名空间中一样,但要在类中使用。让我们看看会是什么样子:

<?php

namespace Bookstore\Domain;

use Bookstore\Utils\Unique;

class Person {
    use Unique;

    protected $firstname;
    protected $surname;
    protected $email;

    public function __construct(
        int $id,
        string $firstname,
        string $surname,
        string $email
    ) {
        $this->firstname = $firstname;
        $this->surname = $surname;
        $this->email = $email;
        $this->setId($id);
    }

    public function getFirstname(): string {
        return $this->firstname;
    }

    public function getSurname(): string {
        return $this->surname;
    }

    public function getEmail(): string {
        return $this->email;
    }

    public function setEmail(string $email) {
        $this->email = $email;
    }
}

我们添加了 use Unique; 语句,让类知道它正在使用该特性。我们删除所有与 ID 相关的内容,甚至是构造函数内部的内容。我们仍然以 ID 作为构造函数的第一个参数,但我们要求特质中的 setId 方法为我们完成所有工作。请注意,我们使用 $this 来引用该方法,就好像该方法是在类内部一样。更新后的层次结构树如下所示(请注意,我们并没有添加所有类或接口的所有方法,这些方法与最近的更改无关,以便尽可能保持图表的小巧和可读性):

image 2023 11 02 13 28 52 640

让我们来看看它是如何工作的,尽管它的工作方式可能是你所期望的。将这段代码添加到 init.php 文件中,包含必要的使用语句,然后在浏览器中执行:

$basic1 = new Basic(1, "name", "surname", "email");
$basic2 = new Basic(null, "name", "surname", "email");
var_dump($basic1->getId()); // 1
var_dump($basic2->getId()); // 2

前面的代码实例化了两个客户。其中第一个客户有一个特定的 ID,而第二个客户则由系统为其选择一个 ID。结果是第二个基本客户的 ID 是 2。这是意料之中的,因为两个客户都是基本客户。但如果客户的类型不同,会发生什么情况呢?

$basic = new Basic(1, "name", "surname", "email");
$premium = new Premium(null, "name", "surname", "email");
var_dump($basic->getId()); // 1
var_dump($premium->getId()); // 2

ID 仍然相同。这是意料之中的,因为该特性包含在 Person 类中,所以静态属性 $lastId 将在 Person 类的所有实例中共享,包括 BasicPremium 客户。如果使用 BasicPremium 客户中的 trait 而不是 Person 类(但不应该使用),结果将如下:

var_dump($basic->getId()); // 1
var_dump($premium->getId()); // 1

每个类都有自己的静态属性。所有 Basic 实例将共享相同的 $lastId,与 Premium 实例的 $lastId 不同。这就清楚地表明,特质中的静态成员与使用它们的类相关联,而不是与特质本身相关联。在测试以下代码时也可以反映出这一点,该代码使用了我们最初的方案,即从 Person 开始使用特质:

$basic = new Basic(1, "name", "surname", "email");
$premium = new Premium(null, "name", "surname", "email");
var_dump(Person::getLastId()); // 2
var_dump(Unique::getLastId()); // 0
var_dump(Basic::getLastId()); // 2
var_dump(Premium::getLastId()); // 2

如果你有一双善于发现问题的眼睛,你可能会开始考虑一些与使用特性有关的潜在问题。如果我们使用了两个包含相同方法的特质,会发生什么?或者,如果你使用的特质包含的方法已经在类中实现了,会发生什么情况?

理想情况下,你应该避免遇到这类情况;它们是可能出现错误设计的警示灯。但由于总会有一些特殊情况,让我们来看看一些孤立的例子,看看它们是如何表现的。

trait 和类实现相同方法的情况很简单。类中显式实现的方法优先,其次是特质中实现的方法,最后是从父类继承的方法。让我们看看它是如何工作的。以下面的特质和类定义为例:

<?php

trait Contract {
    public function sign() {
        echo "Signing the contract.";
    }
}

class Manager {
    use Contract;

    public function sign() {
        echo "Signing a new player.";
    }
}

两者都实现了 sign 方法,这意味着我们必须应用之前定义的优先级规则。类中定义的方法优先于特质中的方法,因此在这种情况下,执行的将是类中的方法:

$manager = new Manager();
$manager->sign(); // Signing a new player.

最复杂的情况是一个类使用了两个具有相同方法的 trait。没有自动解决冲突的规则,因此必须明确地解决冲突。请看下面的代码:

<?php

trait Contract {
    public function sign() {
        echo "Signing the contract.";
    }
}

trait Communicator {
    public function sign() {
        echo "Signing to the waitress.";
    }
}

class Manager {
    use Contract, Communicator;
}

$manager = new Manager();
$manager->sign();

前面的代码会产生一个致命的错误,因为这两个 traits 实现了相同的方法。要选择要使用的 trait,必须使用运算符 insteadof。要使用该操作符,需要说明 trait 名称和要使用的方法,然后是 insteadof 和要拒绝使用的 trait 。也可以使用关键字 as 来添加别名,就像我们使用命名空间时那样,这样就可以同时使用两种方法:

class Manager {
    use Contract, Communicator {
        Contract::sign insteadof Communicator;
        Communicator::sign as makeASign;
    }
}

$manager = new Manager();
$manager->sign(); // Signing the contract.
$manager->makeASign(); // Signing to the waitress.

你可以看到,我们决定使用 Contract 方法而不是 Communicator 方法,但添加了别名,这样两种方法都可以使用。希望你能看到,即使是冲突也是可以解决的,在某些特定情况下,除了处理冲突之外,我们别无他法。