继承

我们曾把面向对象范例说成是解决复杂数据结构的灵丹妙药,尽管我们已经证明可以用属性和方法来定义对象,而且看起来很美很花哨,但这并不是我们用数组所不能解决的问题。封装是使对象比数组更有用的一个特性,但对象的真正威力在于继承。

继承简介

OOP 中的继承是指将类的实现从父类传递给子类的能力。是的,类可以有父类,这一特性的专业说法是一个类从另一个类扩展而来。当扩展一个类时,我们会得到所有未定义为私有的属性和方法,子类可以像使用自己的属性和方法一样使用它们。限制是一个类只能从一个父类扩展。

举个例子,我们来看看 Customer 类。该类包含 firstname, surname, emailid 属性。Customer 实际上是一种特定类型的人,是在我们系统中注册的人,因此他/她可以获取图书。但在我们的系统中还可以有其他类型的人,比如图书管理员或访客。所有这些人都有一些共同的属性,即 firstnamesurname 。因此,如果我们创建一个 Person 类,并从该类扩展出 Customer 类,那就很合理了。层次树的结构如下:

image 2023 11 02 11 29 07 683

请注意 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 类的子类。由于 PersonCustomer 都在同一个命名空间中,因此不必添加任何使用声明,但如果它们不是,则应让它知道如何找到父类。这段代码运行正常,但我们可以看到有一些代码重复。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 扩展而来。让我们创建两种类型的客户:基本客户和高级客户。这两种客户将拥有与 CustomerPerson 相同的属性和方法,以及我们在每种客户中实现的新属性和方法。

将以下代码保存为 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 的层次树将如下所示:

image 2023 11 02 12 03 02 560

我们在这两个类中定义了相同的方法,但它们的实现是不同的。这种方法的目的是不加区分地使用两种类型的客户,而无需知道每次使用的是哪一种。例如,我们可以在 init.php 中暂时使用以下代码。如果没有 Customer 类,请记住添加 use 语句导入该类。

function checkIfValid(Customer $customer, array $books): bool {
    return $customer->getAmountToBorrow() >= count($books);
}

前面的函数将告诉我们某个客户是否可以借阅数组中的所有书籍。请注意,该方法的类型提示是 Customer,但没有指定是哪个客户。该方法将接受 Customer 或任何从 Customer 扩展而来的类,即 BasicPremium 的实例对象。看起来合法吧?那我们就试试使用它:

$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 扩展而来的类都必须实现这些方法,而且这样做是安全的。让我们看看是如何实现的:

image 2023 11 02 12 57 29 585

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