PHP项目抽象类和接口如何选择

wen PHP项目 32

本文目录导读:

PHP项目抽象类和接口如何选择

  1. 核心区别速览
  2. 选择场景:何时用抽象类?
  3. 选择场景:何时用接口?
  4. 决策流程图:一步一步选择
  5. 真实项目中的常见模式
  6. 记住三个关键词

在 PHP 项目中,抽象类(Abstract Class)接口(Interface)是两种核心的面向对象设计工具,选择哪一个,本质上是对“是什么”(is-a)与“能做什么”(can-do)两种关系的权衡

下面从核心区别、适用场景和决策流程三个方面帮你理清思路。


核心区别速览

特性 抽象类 (Abstract) 接口 (Interface)
核心意图 定义“是什么”(is-a关系)
表示子类是父类的一种特殊类型
定义“能做什么”(can-do关系)
表示类具备某种能力或遵循某种规范
继承方式 单继承(一个类只能extends一个抽象类) 多实现(一个类可以implement多个接口)
属性 可以定义属性(成员变量,含访问修饰符) 不能定义属性,只能定义常量(const
方法实现 可以包含已实现的方法和抽象方法 所有方法默认都是抽象的(PHP 8.0+ 可含默认实现,但不推荐
方法可见性 支持 publicprotectedprivate public
构造函数 可以定义构造函数 不能定义构造函数(PHP 8.0 后正式废弃)

选择场景:何时用抽象类?

一句话:当多个类共享相同的核心逻辑、状态(属性)或需要部分默认实现时,用抽象类。

典型场景:

  • 共享代码与状态:多个子类有完全相同的成员变量(如 $name, $price)和相同的公共方法逻辑。
  • 模板方法模式:父类定义算法骨架(一个 public 模板方法),子类只需要实现其中的特定步骤(抽象方法)。
  • 部分实现:父类已经实现了大部分功能,子类只需要覆写或补充少数关键方法。
  • 需要 protectedprivate 方法/属性:抽象类可以隐藏内部实现细节,而接口所有方法都必须公开。

伪代码示例:

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() 方法和构造函数
}

选择场景:何时用接口?

一句话:当定义一组行为规范,且该规范可能被完全不相关的多个类实现时,用接口。

典型场景:

  • 多态能力:需要让不同的类(如 PdfExporterXmlExporterExcelExporter)都支持 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";
    }
}

决策流程图:一步一步选择

你可以按这个流程在脑海中快速判断:

  1. 是否需要定义属性(成员变量)?

    • 是 → 抽象类(接口不能定义属性)
    • 否 → 继续第2步
  2. 是否所有子类都共享大部分相同的逻辑代码?(如构造函数、公共方法实现)

    • 是 → 抽象类(复用代码,减少重复)
    • 否 → 继续第3步
  3. 这个类是否只能属于一个明确的父类?

    • 是(单继承) → 考虑抽象类
    • 否(可能需要实现多个能力) → 接口(多实现)
  4. 最关键的判断:描述的是“是什么关系”还是“能力契约”?

    • “是一个特定的种类”(is-a):Dog is an Animal → 抽象类
    • “具备某种能力”(can-do):Report can be exported → 接口
    • 混合需求:先用抽象类提供核心骨架和共享代码,再让子类实现额外的接口来扩展能力。

真实项目中的常见模式

在实际的 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 框架最常用的模式。

抱歉,评论功能暂时关闭!