本文目录导读:

在 PHP 项目中,抽象类(Abstract Class)和接口(Interface)是两种核心的面向对象设计工具,选择哪一个,本质上是对“是什么”(is-a)与“能做什么”(can-do)两种关系的权衡。
下面从核心区别、适用场景和决策流程三个方面帮你理清思路。
核心区别速览
| 特性 | 抽象类 (Abstract) | 接口 (Interface) |
|---|---|---|
| 核心意图 | 定义“是什么”(is-a关系) 表示子类是父类的一种特殊类型 |
定义“能做什么”(can-do关系) 表示类具备某种能力或遵循某种规范 |
| 继承方式 | 单继承(一个类只能extends一个抽象类) | 多实现(一个类可以implement多个接口) |
| 属性 | 可以定义属性(成员变量,含访问修饰符) | 不能定义属性,只能定义常量(const) |
| 方法实现 | 可以包含已实现的方法和抽象方法 | 所有方法默认都是抽象的(PHP 8.0+ 可含默认实现,但不推荐) |
| 方法可见性 | 支持 public、protected、private |
仅 public |
| 构造函数 | 可以定义构造函数 | 不能定义构造函数(PHP 8.0 后正式废弃) |
选择场景:何时用抽象类?
一句话:当多个类共享相同的核心逻辑、状态(属性)或需要部分默认实现时,用抽象类。
典型场景:
- 共享代码与状态:多个子类有完全相同的成员变量(如
$name,$price)和相同的公共方法逻辑。 - 模板方法模式:父类定义算法骨架(一个
public模板方法),子类只需要实现其中的特定步骤(抽象方法)。 - 部分实现:父类已经实现了大部分功能,子类只需要覆写或补充少数关键方法。
- 需要
protected或private方法/属性:抽象类可以隐藏内部实现细节,而接口所有方法都必须公开。
伪代码示例:
abstract class Animal {
protected string $name;
protected int $age;
// 构造函数(子类可复用)
public function __construct(string $name, int $age) {
$this->name = $name;
$this->age = $age;
}
// 共同的行为(已实现)
public function breathe(): string {
return "{$this->name} is breathing";
}
// 子类必须实现的行为(抽象方法)
abstract public function makeSound(): string;
}
class Dog extends Animal {
public function makeSound(): string {
return "{$this->name} says Woof!";
// 子类可以直接使用 $this->name
}
// 自动继承了 breathe() 方法和构造函数
}
选择场景:何时用接口?
一句话:当定义一组行为规范,且该规范可能被完全不相关的多个类实现时,用接口。
典型场景:
- 多态能力:需要让不同的类(如
PdfExporter、XmlExporter、ExcelExporter)都支持export()方法。 - 解耦与依赖注入:通过接口编程,高层模块不依赖具体类,降低耦合(一个支付系统需要对接支付宝、微信、银联)。
- 多重继承能力:一个类需要同时具备多种能力(如
implements CanWalk, CanSwim)。 - 常量契约:定义一组不可变的常量(如
MAX_ITEMS_PER_PAGE = 20),强制实现类遵守。
伪代码示例:
interface Exportable {
public function export(): string;
}
interface Loggable {
public function log(string $message): void;
}
// 一个与 Animal 无关的类,但可以具备“可导出”能力
class Report implements Exportable {
public function export(): string {
return "Report data as CSV...";
}
}
// 一个类可以实现多个接口
class Invoice implements Exportable, Loggable {
public function export(): string {
return "Invoice data as PDF...";
}
public function log(string $message): void {
echo "[Invoice] $message";
}
}
决策流程图:一步一步选择
你可以按这个流程在脑海中快速判断:
-
是否需要定义属性(成员变量)?
- 是 → 抽象类(接口不能定义属性)
- 否 → 继续第2步
-
是否所有子类都共享大部分相同的逻辑代码?(如构造函数、公共方法实现)
- 是 → 抽象类(复用代码,减少重复)
- 否 → 继续第3步
-
这个类是否只能属于一个明确的父类?
- 是(单继承) → 考虑抽象类
- 否(可能需要实现多个能力) → 接口(多实现)
-
最关键的判断:描述的是“是什么关系”还是“能力契约”?
- “是一个特定的种类”(is-a):
Dog is an Animal→ 抽象类 - “具备某种能力”(can-do):
Report can be exported→ 接口 - 混合需求:先用抽象类提供核心骨架和共享代码,再让子类实现额外的接口来扩展能力。
- “是一个特定的种类”(is-a):
真实项目中的常见模式
在实际的 PHP 项目(Laravel、Symfony)里,你经常看到这种结合使用:
// 1. 先定义接口(能力契约)
interface PackageRepositoryInterface {
public function find(int $id): ?Package;
public function save(Package $package): void;
}
// 2. 再定义抽象类(共享实现骨架)
abstract class BaseRepository implements PackageRepositoryInterface {
protected Database $db;
public function __construct(Database $db) {
$this->db = $db;
}
// 公共实现
public function save(Package $package): void {
$this->db->insert($package->toArray());
}
// 抽象方法,强制子类实现
abstract public function find(int $id): ?Package;
}
// 3. 具体子类
class MysqlPackageRepository extends BaseRepository {
public function find(int $id): ?Package {
// 具体的 MySQL 查询逻辑
}
}
为什么这样设计?
- 接口:定义了契约,让业务逻辑可以解耦地依赖
PackageRepositoryInterface。 - 抽象类:提供了通用的数据库连接和
save()实现,减少重复代码。 - 子类:只关注实现
find()这个最关键的差异化行为。
记住三个关键词
| 工具 | 核心词 | 适用场景 |
|---|---|---|
| 抽象类 | 继承 & 复用 | 代码共享、状态共享、模板方法、单继承链 |
| 接口 | 契约 & 多态 | 行为规范、解耦、多重能力、依赖注入 |
最终建议:
- 如果你的直觉是“这个父类有默认行为和状态”,选抽象类。
- 如果你的直觉是“这个类必须遵循某种规范,但实现方式各不相同”,选接口。
- 两个都不冲突:可以先定义一个接口来搞解耦,再提供一个抽象类来实现默认的公共代码,这也是现代 PHP 框架最常用的模式。