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
来引用该方法,就好像该方法是在类内部一样。更新后的层次结构树如下所示(请注意,我们并没有添加所有类或接口的所有方法,这些方法与最近的更改无关,以便尽可能保持图表的小巧和可读性):

让我们来看看它是如何工作的,尽管它的工作方式可能是你所期望的。将这段代码添加到 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
类的所有实例中共享,包括 Basic
和 Premium
客户。如果使用 Basic
和 Premium
客户中的 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
方法,但添加了别名,这样两种方法都可以使用。希望你能看到,即使是冲突也是可以解决的,在某些特定情况下,除了处理冲突之外,我们别无他法。