接口与抽象类
本课属于 Coddy PHP 学习旅程的“面向对象编程”单元。第 30 课,共 91 课。
现在你已经学习了接口和抽象类,接下来我们来明确什么时候使用它们。它们看起来可能相似,但用途不同,功能也各有特点。
| 特性 | 接口 | 抽象类 |
|---|---|---|
| 方法 | 只有方法签名 | 抽象方法和具体方法都有 |
| 属性 | 只有常量 | 可以有属性 |
| 多重继承 | 一个类可以实现多个接口 | 一个类只能继承一个类 |
| 构造函数 | 可以声明 __construct 签名(没有函数体) | 可以定义带函数体的构造函数 |
当你想定义一个可由无关类实现的契约时,可以使用接口。例如,Printer 和 Logger 都可以实现 Writable 接口,即使它们没有共同的祖先类。
当你有一些相关的类需要共享通用代码时,可以使用抽象类。如果你的 Dog 类和 Cat 类都需要相同的 eat() 方法实现,抽象 Animal 类可以让你只编写一次这段代码。
你也可以结合这两种方式。一个类可以继承抽象类,同时实现多个接口:
<?php
abstract class Animal {
protected $name;
public function eat() {
return $this->name . " is eating";
}
}
interface Swimmable {
public function swim();
}
class Duck extends Animal implements Swimmable {
public function __construct($name) {
$this->name = $name;
}
public function swim() {
return $this->name . " is swimming";
}
}
关键要点:当你需要在互不相关的类之间定义功能时,选择接口。当你需要在相关类之间共享代码时,选择抽象类。
挑战
简单让我们构建一个车辆系统,演示何时使用抽象类而不是接口,以及如何有效地结合这两种方式。
你将创建一个系统,让车辆通过继承共享通用属性,同时也能通过接口获得额外能力。这反映了现实世界中的设计决策:汽车“是一种”车辆(继承),但它也“具备”加油能力(接口)。
将代码分布在 4 个文件中:
Refuelable.php:定义一个Refuelable接口,其中包含一个方法签名:refuel($amount)。任何需要加油的类都可以具备这项能力,不受其继承层级的限制。Vehicle.php:定义一个抽象Vehicle类,为所有车辆提供共享功能。它应有一个受保护的$brand属性和一个受保护的$fuelLevel属性(初始值为 0)。构造函数接收品牌名称。添加一个具体方法getBrand(),用于返回品牌;添加一个具体方法getFuelLevel(),用于返回当前油量;再添加一个抽象方法drive(),供子类实现。Car.php:创建一个继承Vehicle并实现Refuelable的Car类。在文件顶部引入这两个必需的文件。构造函数接收品牌,并将其传递给父类构造函数。实现drive(),使其返回"[brand] car is driving"。实现refuel($amount),将加油量加到油量中,并返回"[brand] refueled to [fuelLevel] liters"。main.php:引入 Car 文件。你将收到两个输入:品牌名称和加油量(将其转换为整数)。使用该品牌创建一个Car。在第一行打印drive()的结果,然后在第二行打印传入加油量的refuel()结果。
这个挑战说明了两者的关键区别:抽象 Vehicle 类共享代码并建立“一种”关系,而 Refuelable 接口则添加了一项能力,其他不相关的类(例如发电机或船)也可以具备这项能力,而不必是车辆。
亲手试试
<?php
require_once 'Car.php';
// 读取输入
$brand = trim(fgets(STDIN));
$fuelAmount = intval(trim(fgets(STDIN)));
// TODO:用品牌创建一个 Car 对象
// TODO:在第一行打印 drive() 的结果
// TODO:在第二行打印传入燃油量后 refuel() 的结果
?>这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: PHP 在线编译器