在 PHP 面试中,接口和抽象类几乎是必问的题目。很多候选人能背出“接口不能有实现,抽象类可以有实现”“一个类只能继承一个抽象类,但可以实现多个接口”这类教科书答案。然而,一旦面试官追问“那你在真实业务中怎么选”,能答清楚的人就少了一大半。这篇文章从设计本质出发,结合真实业务场景,帮你彻底理清接口与抽象类的选择逻辑。
一、先厘清概念:它们到底解决什么问题
接口:定义“能做什么”
接口是一份契约,它只声明方法签名,不关心谁来兑现、怎么兑现。任何类只要实现了接口,就对外承诺“我具备这些能力”。
interface Payable
{
public function pay(float $amount): bool;
public function refund(string $tradeNo): bool;
}
接口的核心价值在于解耦调用方与实现方。调用方只依赖 Payable 这个契约,至于背后是微信支付、支付宝还是银联,它一概不关心。
抽象类:定义“是什么,以及部分怎么做”
抽象类表达的是一种“is-a”的继承关系。它既可以声明抽象方法(子类必须实现),也可以提供具体方法的默认实现,还可以定义属性、构造函数等。
abstract class AbstractPayment
{
protected string $appId;
public function __construct(string $appId)
{
$this->appId = $appId;
}
abstract public function pay(float $amount): bool;
protected function sign(array $params): string
{
// 通用签名逻辑,所有子类复用
ksort($params);
return md5(http_build_query($params) . $this->appId);
}
}
抽象类的核心价值在于代码复用 + 强制约束。它把多个子类共有的逻辑上提到父类,避免重复代码。
二、设计差异对比:不只是语法区别
| 维度 | 接口 | 抽象类 |
|---|---|---|
| 继承数量 | 可实现多个 | 只能继承一个 |
| 方法实现 | 不能有具体实现(PHP 8 前) | 可以有具体实现 |
| 属性 | 只能有常量 | 可以有任意属性 |
| 构造函数 | 不能声明 | 可以声明 |
| 访问修饰符 | 方法必须 public | 可用 protected/private |
| 语义关系 | can-do(具备能力) | is-a(属于某类事物) |
| 版本兼容 | 新增方法会破坏所有实现类 | 新增具体方法不影响子类 |
PHP 8.0 之后接口可以定义常量,但依然不能有实例属性,也不能有方法体(除非是 static 方法的默认实现提案,目前尚未落地)。这个限制恰恰是接口保持纯粹契约性的关键。
三、真实业务中的选择标准
面试中最能拉开差距的,是你能否给出可操作的决策依据。以下四条标准可以直接用于回答。
标准一:是否存在共享代码
如果多个实现类之间有大量重复逻辑需要复用,优先抽象类。例如一个支付模块,微信和支付宝的签名、参数组装、HTTP 请求发送逻辑高度相似,只有请求地址和参数格式不同,这时抽象类是更好的选择。
如果各个实现类之间毫无共同逻辑,只是对外暴露统一的能力,用接口。例如日志驱动:文件日志、Redis 日志、Syslog 日志的内部实现完全不同,但它们都需要实现 write() 和 read(),接口就够了。
标准二:是否需要多继承语义
PHP 不支持类的多继承。如果一个类需要同时具备多种能力——比如一个 OrderService 既要是 Jsonable,又要是 Cacheable,还要是 Loggable——那只能用接口。抽象类在这里无能为力。
标准三:是否面向扩展开放
这是开闭原则的直接体现。如果你在设计一个 SDK 或框架,预期第三方会扩展你的功能,接口更安全。因为接口一旦发布,新增方法会破坏所有已有实现(除非用默认方法,但 PHP 不支持)。而抽象类新增一个具体方法,所有子类自动继承,不会破坏兼容性。
反过来,如果你希望严格控制子类的行为,不允许外部随意扩展,抽象类可以通过 final 关键字锁定关键方法,接口做不到这一点。
标准四:依赖方向是“向上”还是“向外”
依赖倒置原则告诉我们:高层模块不应依赖低层模块,二者都应依赖抽象。这里的“抽象”既可以是接口,也可以是抽象类。但实践中有一个经验法则:
- 跨层调用用接口:Controller 调用 Service、Service 调用 Repository,用接口定义边界,方便单元测试打桩。
- 同层复用用抽象类:同一层内多个实现共享逻辑,用抽象类提取公共部分。
四、一个真实业务案例:订单导出
假设你正在开发一个订单导出功能,支持导出为 CSV、Excel、PDF 三种格式。
第一步:识别共同点。 三种格式都需要:查询订单数据、格式化数据、写入文件、返回下载链接。其中“查询订单数据”和“返回下载链接”逻辑完全一致,“格式化数据”和“写入文件”各不相同。
第二步:设计。 用抽象类 AbstractOrderExporter 实现查询和返回链接,声明抽象方法 format() 和 write()。三种格式分别继承并实现这两个方法。
abstract class AbstractOrderExporter
{
public function export(array $orderIds): string
{
$orders = $this->fetchOrders($orderIds);
$data = $this->format($orders);
$path = $this->write($data);
return $this->buildDownloadUrl($path);
}
abstract protected function format(array $orders): string;
abstract protected function write(string $data): string;
protected function fetchOrders(array $ids): array { /* 通用查询 */ }
protected function buildDownloadUrl(string $path): string { /* 通用链接 */ }
}
第三步:考虑扩展性。 如果未来要支持导出到第三方云存储(OSS、S3),而云存储的写入方式和本地文件完全不同,这时可以再定义一个 StorageInterface,让导出器依赖接口而非具体存储。
这就是真实业务中的典型做法:抽象类负责“模板方法”式的流程复用,接口负责“策略”式的可替换实现。 两者不是二选一,而是配合使用。
五、面试回答模板
如果面试官问“接口和抽象类你怎么选”,可以这样组织回答:
- 先说本质:接口定义契约(can-do),抽象类定义模板(is-a)。
- 再说语法约束:单继承 vs 多实现、是否有方法体、是否有属性。
- 重点说选择标准:有共享代码用抽象类,需要多能力用接口,面向扩展用接口,同层复用用抽象类。
- 最后用案例收尾:模板方法模式用抽象类,策略模式用接口,两者常配合出现。
这样的回答既有理论深度,又有工程落地感,远胜于背诵语法差异。
结语
接口和抽象类的选择,本质上是在“契约的纯粹性”和“代码的复用性”之间做权衡。面试官想听到的不是你记住了多少区别,而是你能否在具体业务中做出合理的设计决策。下次面试时,试着用“共享代码、多继承、扩展性、依赖方向”这四个维度去分析,你的回答会立刻上一个台阶。
未经允许不得转载:任鹏个人博客 » PHP 面试精讲:接口与抽象类的设计差异及真实业务中的选择标准

