接口

接口是一种 OOP 元素,它将一组函数声明组合在一起,但不实现这些函数,也就是说,它指定了名称、返回类型和参数,但不指定代码块。接口不同于抽象类,因为接口不能包含任何实现,而抽象类可以混合方法定义和实现。接口的目的是说明类能做什么,而不是如何做。

从我们的代码中,我们可以发现接口的一种潜在用法。客户有一个预期的行为,但其实现会根据客户的类型而改变。因此,客户可以是一个接口,而不是一个抽象类。但是,由于接口不能实现任何功能,也不能包含属性,我们必须将具体代码从 Customer 类转移到其他地方。现在,让我们把它移到 Person 类中。如图所示,编辑 Person 类:

<?php

namespace Bookstore\Domain;

class Person {

    private static $lastId = 0;
    protected $id;
    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;

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

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

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

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

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

    public function getEmail(): string {
        return $this->email;
    }
}
让事情变得过于复杂

界面非常有用,但任何事物都有其存在的理由和时机。由于我们的应用程序是一个非常简单的说教式应用程序,因此并不适合使用接口。上一节中已经定义的抽象类是我们的最佳方案。但为了展示接口是如何工作的,我们将对代码进行调整。

不过不用担心,在第 5 章 "使用数据库" 和第 6 章 "适应 MVC" 中介绍数据库和 MVC 模式后,我们现在要介绍的大部分代码都将被更好的实践所取代。

在编写自己的应用程序时,不要试图将事情复杂化。开发人员经常会编写非常复杂的代码,试图在一个非常简单的场景中展示他们所掌握的所有技能。只使用必要的工具,留下易于维护的简洁代码,当然,这些代码也能按预期运行。

使用以下内容更改 Customer.php 的内容:

<?php

namespace Bookstore\Domain;

interface Customer {
    public function getMonthlyFee(): float;
    public function getAmountToBorrow(): int;
    public function getType(): string;
}

请注意,接口与抽象类非常相似。不同之处在于它是用关键字 interface 定义的,而且它的方法没有 abstract 字样。接口不能实例化,因为它们的方法不像抽象类那样可以实现。你唯一能做的就是创建一个类来实现它们。

实现接口意味着实现接口中定义的所有方法,就像我们扩展抽象类一样。它具有抽象类扩展的所有优点,例如属于该类型—​在类型提示时非常有用。从开发者的角度来看,使用实现了接口的类就像编写了一份合同:你可以确保你的类始终拥有接口中声明的方法,无论其实现如何。因此,接口只关心公共方法,也就是其他开发人员可以使用的方法。你在代码中需要做的唯一改动就是用 implements 代替关键字 extends

class Basic implements Customer {

那么,如果我们可以一直使用抽象类,不仅可以强制实现方法,还可以继承代码,为什么还要使用接口呢?原因在于,你只能从一个类扩展,但可以同时实现多个实例。试想一下,如果你有另一个定义付款人的接口。这样就可以识别有支付能力的人,无论其支付能力如何。将以下代码保存在 src/Domain/Payer.php 中:

<?php

namespace Bookstore\Domain;

interface Payer {
    public function pay(float $amount);
    public function isExtentOfTaxes(): bool;
}

现在我们的基本客户和高级客户都可以实现这两个接口。基本客户如下所示:

//...
use Bookstore\Domain\Customer;
use Bookstore\Domain\Person;

class Basic extends Person implements Customer {
    public function getMonthlyFee(): float {
//...

优质客户也会以同样的方式发生变化:

//...
use Bookstore\Domain\Customer;
use Bookstore\Domain\Person;

class Premium extends Person implements Customer {
    public function getMonthlyFee(): float {
//...

你会发现,这段代码将不再有效。原因是虽然我们实现了第二个接口,但方法却没有实现。将这两个方法添加到基本客户类中:

public function pay(float $amount) {
    echo "Paying $amount.";
}

public function isExtentOfTaxes(): bool {
    return false;
}

将这两个方法添加到高级客户类中:

public function pay(float $amount) {
    echo "Paying $amount.";
}

public function isExtentOfTaxes(): bool {
    return true;
}

如果您知道所有客户都必须是付款人,您甚至可以使 Customer 接口继承 Payer 接口:

interface Customer extends Payer {

这一更改完全不会影响我们的类的使用。其他开发人员会看到,我们的基本客户和高级客户继承自 PayerCustomer,因此它们包含所有必要的方法。这些接口是独立的,还是相互扩展,这不会有太大影响。

接口只能从其他接口扩展,而类只能从其他类扩展。只有当一个类实现了一个接口时,它们才能混合在一起,但类既不能从接口扩展,接口也不能从类扩展。但从类型提示的角度来看,它们可以互换使用。

为了总结本节内容并使问题更加清晰,让我们展示一下新增内容后的层次树。与抽象类一样,接口中声明的方法在接口中显示,而不是在实现接口的每个类中显示。

image 2023 11 02 13 17 12 557

多态性

多态性(Polymorphism)是一种 OOP 特性,它允许我们使用实现相同接口的不同类。它是面向对象编程的魅力之一。它允许开发人员创建由类和层次树组成的复杂系统,但提供了一种简单的方法来处理它们。

想象一下,我们有一个函数,给定一个付款人,检查它是否免税,并让它支付一定数额的钱。这段代码并不介意付款人是顾客、图书管理员还是与书店无关的人。它唯一关心的是付款人是否有能力付款。函数如下:

function processPayment(Payer $payer, float $amount) {
    if ($payer->isExtentOfTaxes()) {
        echo "What a lucky one...";
    } else {
        $amount *= 1.16;
    }
    $payer->pay($amount);
}

您可以向该函数发送基本客户或高级客户,其行为将有所不同。但是,由于两者都实现了 Payer 接口,因此提供的两个对象都是有效类型,都能执行所需的操作。

checkIfValid 函数接收一个客户和一个 books 列表。我们已经看到,发送任何类型的客户都能使函数按预期运行。但是,如果我们发送一个从 Payer 扩展而来的 Librarian 类对象,会发生什么情况呢?由于 Payer 不知道 Customer(恰恰相反),函数会抱怨类型提示没有完成。

PHP 自带的一个有用功能是检查对象是否是特定类或接口的实例。使用方法是在变量后面指定 instanceof 关键字以及类或接口的名称。它返回一个布尔值,如果对象来自于扩展或实现了指定类的类,则返回 true,否则返回 false。我们来看几个例子:

$basic = new Basic(1, "name", "surname", "email");
$premium = new Premium(2, "name", "surname", "email");
var_dump($basic instanceof Basic); // true
var_dump($basic instanceof Premium); // false
var_dump($premium instanceof Basic); // false
var_dump($premium instanceof Premium); // true
var_dump($basic instanceof Customer); // true
var_dump($basic instanceof Person); // true
var_dump($basic instanceof Payer); // true

请记住为每个类或接口添加所有 use 语句,否则 PHP 将认为指定的类名位于文件的命名空间内。