继承
我们曾把面向对象范例说成是解决复杂数据结构的灵丹妙药,尽管我们已经证明可以用属性和方法来定义对象,而且看起来很美很花哨,但这并不是我们用数组所不能解决的问题。封装是使对象比数组更有用的一个特性,但对象的真正威力在于继承。
继承简介
OOP 中的继承是指将类的实现从父类传递给子类的能力。是的,类可以有父类,这一特性的专业说法是一个类从另一个类扩展而来。当扩展一个类时,我们会得到所有未定义为私有的属性和方法,子类可以像使用自己的属性和方法一样使用它们。限制是一个类只能从一个父类扩展。
举个例子,我们来看看 Customer
类。该类包含 firstname
, surname
, email
和 id
属性。Customer
实际上是一种特定类型的人,是在我们系统中注册的人,因此他/她可以获取图书。但在我们的系统中还可以有其他类型的人,比如图书管理员或访客。所有这些人都有一些共同的属性,即 firstname
和 surname
。因此,如果我们创建一个 Person
类,并从该类扩展出 Customer
类,那就很合理了。层次树的结构如下:

请注意 Customer
是如何连接到 Person
的。Person
中的方法没有在 Customer
中定义,因为它们是从扩展中隐含出来的。现在,按照我们的惯例将新类保存在 src/Domain/Person.php
中:
<?php
namespace Bookstore\Domain;
class Person {
protected $firstname;
protected $surname;
public function __construct(string $firstname, string $surname) {
$this->firstname = $firstname;
$this->surname = $surname;
}
public function getFirstname(): string {
return $this->firstname;
}
public function getSurname(): string {
return $this->surname;
}
}
前面代码片段中定义的类看起来并不特别;我们只是定义了两个属性、一个构造函数和两个获取函数。但请注意,我们将属性定义为 protected
,因为如果定义为 private
,子类将无法访问它们。现在,我们可以删除重复的属性及其获取器,更新客户类:
<?php
namespace Bookstore\Domain;
class Customer extends Person {
private static $lastId = 0;
private $id;
private $email;
public function __construct(
int $id,
string $name,
string $surname,
string $email
) {
if (empty($id)) {
$this->id = ++self::$lastId;
} else {
$this->id = $id;
if ($id > self::$lastId) {
self::$lastId = $id;
}
}
$this->name = $name;
$this->surname = $surname;
$this->email = $email;
}
public static function getLastId(): int {
return self::$lastId;
}
public function getId(): int {
return $this->id;
}
public function getEmail(): string {
return $this->email;
}
public function setEmail($email): string {
$this->email = $email;
}
}
注意新关键字 extends
;它告诉 PHP 这个类是 Person
类的子类。由于 Person
和 Customer
都在同一个命名空间中,因此不必添加任何使用声明,但如果它们不是,则应让它知道如何找到父类。这段代码运行正常,但我们可以看到有一些代码重复。Customer
类的构造函数与 Person
类的构造函数做了同样的工作!我们将尽快解决这个问题。
为了从子类中引用父类的方法或属性,可以使用 $this
,就好像该属性或方法是在同一个类中一样。事实上,你可以说它确实是。但是 PHP 允许在子类中重新定义父类中已经存在的方法。如果要引用父类的实现,就不能使用 $this
,因为 PHP 会调用子类中的实现。要强制 PHP 使用父类的方法,请使用关键字 parent::
而不是 $this
。更新客户类的构造函数如下:
public function __construct(
int $id,
string $firstname,
string $surname,
string $email
) {
parent::__construct($firstname, $surname);
if (empty($id)) {
$this->id = ++self::$lastId;
} else {
$this->id = $id;
if ($id > self::$lastId) {
self::$lastId = $id;
}
}
$this->email = $email;
}
这个新的构造函数不会重复代码。相反,它调用了父类 Person
的构造函数,发送了 $firstname
和 $surname
,让父类做它已经知道如何做的事情。这样不仅避免了代码重复,而且还方便了将来对 Person
的构造函数进行修改。如果我们需要更改 Person
的构造函数的实现,我们只需在一个地方进行更改,而不是在所有子代中。
覆盖方法
如前所述,当从一个类扩展时,我们会得到父类的所有方法。这是隐含的,因此它们实际上并没有写入子类中。如果你实现了另一个具有相同签名和/或名称的方法,会发生什么呢?你将覆盖该方法。
由于我们不需要在我们的类中使用这一功能,所以我们只需在 init.php
文件中添加一些代码来显示这一行为,然后就可以直接删除它了。让我们定义一个 Pops
类和一个从父类扩展而来的 Child
类,并在这两个类中都定义一个 sayHi
方法:
class Pops {
public function sayHi() {
echo "Hi, I am pops.";
}
}
class Child extends Pops{
public function sayHi() {
echo "Hi, I am a child.";
}
}
$pops = new Pops();
$child = new Child();
echo $pops->sayHi(); // Hi, I am pops.
echo $child->sayHi(); // Hi, I am Child.
高亮显示的代码说明该方法已被重载,因此从子代的角度调用该方法时,我们将使用它而不是从父代继承的方法。但是,如果我们也想引用继承的方法,该怎么办呢?我们可以使用关键字 parent
来引用它。让我们看看它是如何工作的:
class Child extends Pops{
public function sayHi() {
echo "Hi, I am a child.";
parent::sayHi();
}
}
$child = new Child();
echo $child->sayHi(); // Hi, I am Child. Hi I am pops.
现在,孩子既要向自己问好,也要向爸爸问好。这看起来非常简单方便,对吗?但这是有限制的。想象一下,在现实生活中,孩子非常害羞,他不会对每个人打招呼。我们可以尝试将该方法的可见性设置为受保护,但看看会发生什么:
class Child extends Pops{
protected function sayHi() {
echo "Hi, I am a child.";
}
}
在尝试这段代码时,即使不对其进行实例化,也会出现抱怨该方法访问级别的致命错误。原因是在覆盖时,方法的可见性必须至少与继承方法的可见性相同。也就是说,如果我们继承了一个受保护的方法,我们可以用另一个受保护的或公共的方法来覆盖它,但绝不能用一个私有的方法。
抽象类
请记住,每次只能从一个父类扩展。也就是说,Customer
只能从 Person
扩展。但如果我们想让这棵层次树变得更复杂,我们可以创建从 Customer
扩展而来的子类,这些子类也将隐式地从 Person
扩展而来。让我们创建两种类型的客户:基本客户和高级客户。这两种客户将拥有与 Customer
和 Person
相同的属性和方法,以及我们在每种客户中实现的新属性和方法。
将以下代码保存为 src/Domain/Customer/Basic.php
:
<?php
namespace Bookstore\Domain\Customer;
use Bookstore\Domain\Customer;
class Basic extends Customer {
public function getMonthlyFee(): float {
return 5.0;
}
public function getAmountToBorrow(): int {
return 3;
}
public function getType(): string {
return 'Basic';
}
}
以下代码为 src/Domain/Customer/Premium.php
:
<?php
namespace Bookstore\Domain\Customer;
use Bookstore\Domain\Customer;
class Premium extends Customer {
public function getMonthlyFee(): float {
return 10.0;
}
public function getAmountToBorrow(): int {
return 10;
}
public function getType(): string {
return 'Premium';
}
}
在前面两段代码中需要注意的是,我们从两个不同的类中扩展了 Customer
,而且这是完全合法的—我们可以从不同命名空间的类中扩展。这样,Person
的层次树将如下所示:

我们在这两个类中定义了相同的方法,但它们的实现是不同的。这种方法的目的是不加区分地使用两种类型的客户,而无需知道每次使用的是哪一种。例如,我们可以在 init.php
中暂时使用以下代码。如果没有 Customer
类,请记住添加 use 语句导入该类。
function checkIfValid(Customer $customer, array $books): bool {
return $customer->getAmountToBorrow() >= count($books);
}
前面的函数将告诉我们某个客户是否可以借阅数组中的所有书籍。请注意,该方法的类型提示是 Customer
,但没有指定是哪个客户。该方法将接受 Customer
或任何从 Customer
扩展而来的类,即 Basic
或 Premium
的实例对象。看起来合法吧?那我们就试试使用它:
$customer1 = new Basic(5, 'John', 'Doe', 'johndoe@mail.com');
var_dump(checkIfValid($customer1, [$book1])); // ok
$customer2 = new Customer(7, 'James', 'Bond', 'james@bond.com');
var_dump(checkIfValid($customer2, [$book1])); // fails
第一次调用按预期运行,但第二次调用失败,尽管我们发送的是一个 Customer
对象。问题出现的原因是父节点不知道任何 getAmountToBorrow
方法!此外,我们依赖子代始终实现该方法看起来也很危险。解决方案在于使用抽象类。
抽象类是一个无法实例化的类。它的唯一目的是确保其子类能正确实现。使用关键字 abstract
将类声明为抽象类,然后再定义一个普通类。我们还可以指定子类必须实现的方法,而无需在父类中实现这些方法。这些方法称为抽象方法,定义时在开头使用关键字 abstract
。当然,其余的普通方法也可以留在父类中,并由子类继承:
<?php
abstract class Customer extends Person {
//...
abstract public function getMonthlyFee();
abstract public function getAmountToBorrow();
abstract public function getType();
//...
}
前面的抽象解决了这两个问题。首先,我们将无法发送 Customer
类的任何实例,因为我们无法将其实例化。这意味着 checkIfValid
方法将接受的所有对象都只能是 Customer
的子类。另一方面,声明抽象方法会强制所有扩展类的子类实现这些方法。这样,我们就能确保所有对象都能实现 getAmountToBorrow
方法,我们的代码也就安全了。
新的层次结构树将在 Customer
中定义三个抽象方法,而在其子类中省略这些方法。的确,我们是在其子类中实现了这些方法,但由于它们是由 Customer
强制执行的,而且由于抽象性,我们确信所有从 Customer
扩展而来的类都必须实现这些方法,而且这样做是安全的。让我们看看是如何实现的:

在添加了最后一个新类后,您的 init.php
文件应该会失败。原因是它试图实例化 Customer
类,但现在它是抽象类,所以无法实例化。实例化一个具体类,即一个非抽象类,就可以解决问题。