接口
接口是一种 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 {
这一更改完全不会影响我们的类的使用。其他开发人员会看到,我们的基本客户和高级客户继承自 Payer
和 Customer
,因此它们包含所有必要的方法。这些接口是独立的,还是相互扩展,这不会有太大影响。
接口只能从其他接口扩展,而类只能从其他类扩展。只有当一个类实现了一个接口时,它们才能混合在一起,但类既不能从接口扩展,接口也不能从类扩展。但从类型提示的角度来看,它们可以互换使用。
为了总结本节内容并使问题更加清晰,让我们展示一下新增内容后的层次树。与抽象类一样,接口中声明的方法在接口中显示,而不是在实现接口的每个类中显示。

多态性
多态性(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 将认为指定的类名位于文件的命名空间内。