【中国观察2026年08月26日】
在 PHP 面向对象开发中,abstract 抽象类经常出现在框架、数据库组件、日志系统、支付接口、文件存储和插件架构中。它解决的并不是“代码不能直接使用”这么简单的问题,而是当多个类具有共同的状态、共同的代码和共同的行为约束时,通过一个基类把这些内容组织起来,同时要求具体子类完成各自不同的部分。
理解抽象类最简单的方法,是先理解继承关系。假设系统需要支持 MySQL、PostgreSQL 和 SQLite 三种数据库驱动,它们都需要连接数据库、执行查询、关闭连接,但具体实现方式可能不同。开发者可以把所有数据库驱动共同需要的结构放进一个抽象基类,再让具体驱动继承它。
一个最简单的例子是:
abstract class DatabaseHandler
{
abstract public function connect(): void;
abstract public function query(string $sql): mixed;
public function disconnect(): void
{
echo "关闭数据库连接";
}
}
这里的 DatabaseHandler 是抽象类。它可以拥有普通属性和普通方法,也可以拥有抽象方法。connect() 和 query() 没有方法体,因此要求子类提供具体实现,而 disconnect() 已经提供了一套所有子类都可以继承的默认行为。
然后创建具体的 MySQL 实现:
class MySQLHandler extends DatabaseHandler
{
public function connect(): void
{
echo "连接 MySQL";
}
public function query(string $sql): mixed
{
echo "执行 MySQL 查询:{$sql}";
return null;
}
}
实例化具体子类:
$db = new MySQLHandler();
$db->connect();
$db->query("SELECT * FROM users");
$db->disconnect();
这里最重要的关系是:MySQLHandler 是一个具体实现,而 DatabaseHandler 描述的是这一组数据库处理器必须具备什么能力以及可以共享什么基础逻辑。
抽象类本身不能直接实例化:
$db = new DatabaseHandler();
这会产生致命错误,因为抽象类不是一个完整的具体对象类型。它的职责是作为其他类的基础,而不是直接创建对象。
抽象方法到底是什么
抽象方法的核心作用是建立一个必须遵守的契约。
例如:
abstract class PaymentGateway
{
abstract public function pay(float $amount): bool;
}
如果创建:
class StripeGateway extends PaymentGateway
{
public function pay(float $amount): bool
{
return true;
}
}
那么 StripeGateway 必须实现 pay()。
如果直接写:
class StripeGateway extends PaymentGateway
{
}
PHP 会认为这个子类仍然没有完成抽象方法的实现,因此这个类本身也必须声明为 abstract,否则会产生错误。
这里有一个重要区别:抽象类并不意味着里面所有方法都是抽象方法。一个抽象类完全可以同时包含抽象方法和已经实现的普通方法。
例如:
abstract class Report
{
abstract protected function generateContent(): string;
public function output(): void
{
echo $this->generateContent();
}
}
子类只需要负责生成具体内容,而输出逻辑已经由父类统一完成。
这实际上已经接近一个非常实用的设计模式,即模板方法模式。父类定义整体执行流程,子类只实现其中变化的步骤。
例如:
abstract class Report
{
public function generate(): string
{
$data = $this->loadData();
return $this->format($data);
}
abstract protected function loadData(): array;
abstract protected function format(array $data): string;
}
PDF 报告、HTML 报告或者 CSV 报告都可以继承这个类,但每一种报告可以拥有不同的数据获取和格式化方式。
这种情况下,抽象类的价值就非常明显:共同流程只写一次,变化部分交给子类实现。
抽象类和接口有什么区别
PHP 开发中另一个经常出现的概念是 interface。
接口主要用于描述一个类必须具备哪些公开行为,而抽象类除了能够定义行为之外,还能够提供共享的实现、属性以及受保护的内部逻辑。
例如:
interface LoggerInterface
{
public function log(string $message): void;
}
然后可以让不同类型的日志系统实现这个接口:
class FileLogger implements LoggerInterface
{
public function log(string $message): void
{
file_put_contents(
'app.log',
$message . PHP_EOL,
FILE_APPEND
);
}
}
数据库日志、文件日志、远程日志服务都可以实现同一个接口。
接口解决的是“必须提供什么能力”,而抽象类更适合解决“这些对象拥有共同基础,同时部分行为需要由子类完成”。
两者也可以组合使用。
interface CacheInterface
{
public function get(string $key): mixed;
public function set(string $key, mixed $value): void;
}
然后:
abstract class BaseCache implements CacheInterface
{
protected int $ttl = 3600;
protected function normalizeKey(string $key): string
{
return trim(strtolower($key));
}
}
Redis、Memcached 或其他缓存实现可以继续继承 BaseCache。
class RedisCache extends BaseCache
{
public function get(string $key): mixed
{
$key = $this->normalizeKey($key);
return null;
}
public function set(string $key, mixed $value): void
{
$key = $this->normalizeKey($key);
}
}
这样设计以后,接口负责对外定义能力,抽象类负责提供共享实现,而具体子类负责处理具体技术。
PHP 中抽象类的继承限制
抽象方法的可见性和方法签名也需要遵守继承规则。
例如父类:
abstract class UserRepository
{
abstract public function findById(int $id): ?array;
}
子类实现时必须保持兼容:
class MySQLUserRepository extends UserRepository
{
public function findById(int $id): ?array
{
return null;
}
}
不能把父类要求的 public 方法随意降低为 protected 或 private。
同样,参数类型、返回类型等也需要符合 PHP 的继承兼容规则。大型项目中使用严格类型声明,可以让这类设计约束更加清晰:
declare(strict_types=1);
抽象类并不会自动让代码更加安全或者更加优秀,它只是给继承体系增加了一层结构约束。
final 和 abstract 并不是一回事
原稿中把 final 与 PHP 8 的抽象类联系起来,这里需要纠正。
final 的作用是阻止继续继承或者覆盖,而 abstract 的作用是要求这个类或者方法由更具体的实现完成。
因此:
abstract class BaseController
{
}
表示这个类不能直接实例化,但可以被继承。
而:
final class SecurityService
{
}
表示这个类不能被其他类继承。
这两个概念解决的是不同问题。
更重要的是,不能把 abstract 和 final 简单组合成“一个不能实例化又不能继承的类”来作为普通设计方案。这样的组合失去了抽象基类的主要意义。需要禁止扩展时使用 final,需要建立可继承的抽象基础时使用 abstract。
抽象类不一定比普通类高级
这是实际项目中特别容易出现的问题。
开发者学习面向对象以后,很容易认为:
“既然抽象类能够限制子类,那就应该尽量使用抽象类。”
实际上没有这种规则。
如果一个类本身就是完整的业务对象,并且没有必要要求其他类实现某些方法,就应该使用普通类。
例如:
class User
{
public function getName(): string
{
return 'Brian';
}
}
没有任何理由为了“面向对象设计”而把 User 写成抽象类。
同样,如果只是希望定义一组独立能力,而且这些能力之间不存在共享状态和基础实现,接口通常比抽象类更加合适。
抽象类什么时候特别有价值
抽象类比较适合几种典型场景。
第一种是多个实现拥有明显的共同代码。
例如不同支付渠道都需要记录订单号、验证金额和记录日志,这些逻辑可以放在抽象基类中,而支付渠道各自实现发送请求的部分。
第二种是需要模板方法。
父类规定:
验证参数
创建订单
执行支付
记录结果
子类只负责实现具体支付操作。
第三种是需要共享状态。
例如不同缓存驱动都需要保存 TTL、命名空间或者连接配置,这些属性可以由抽象基类统一管理。
第四种是希望建立受控的继承体系。
例如框架允许开发者扩展 BaseController,但要求所有控制器必须实现某几个核心方法。
抽象类并不能替代组合
现代 PHP 项目中还有一个非常重要的设计原则,就是不要因为能够继承,就把所有东西都做成继承关系。
假设一个订单系统需要日志、缓存、数据库和消息队列,没有必要设计成:
Order
|
BaseService
|
DatabaseService
|
LoggerService
这种继承关系很容易变得混乱。
更合理的方式通常是组合:
class OrderService
{
public function __construct(
private UserRepository $users,
private LoggerInterface $logger,
private CacheInterface $cache
) {
}
}
OrderService 不需要继承日志类或者缓存类,而是拥有这些依赖。
这就是组合优于滥用继承的典型场景。
抽象类可以用于类型提示
抽象类还有一个非常实用的地方,就是可以作为参数类型。
例如:
function processDatabase(DatabaseHandler $db): void
{
$db->connect();
}
只要对象属于 DatabaseHandler 的具体子类,就可以传入:
$mysql = new MySQLHandler();
processDatabase($mysql);
这使业务代码不需要关心具体使用的是哪个数据库实现。
同样,接口也可以用于类型提示:
function writeLog(LoggerInterface $logger): void
{
$logger->log('系统启动');
}
只要对象实现 LoggerInterface,就可以传入。
这种设计能够减少业务代码对具体实现类的依赖,也是依赖倒置和依赖注入在 PHP 项目中经常出现的基础。
工厂模式中如何使用抽象类
抽象类也经常与工厂模式结合。
例如:
abstract class Exporter
{
abstract public function export(array $data): string;
protected function validate(array $data): void
{
if ($data === []) {
throw new InvalidArgumentException('数据不能为空');
}
}
}
CSV:
class CsvExporter extends Exporter
{
public function export(array $data): string
{
$this->validate($data);
return 'CSV DATA';
}
}
JSON:
class JsonExporter extends Exporter
{
public function export(array $data): string
{
$this->validate($data);
return json_encode($data);
}
}
调用方只需要依赖 Exporter:
function generateFile(Exporter $exporter, array $data): string
{
return $exporter->export($data);
}
具体使用 CSV 还是 JSON,可以交给工厂或者配置系统决定。
这样做以后,业务逻辑不需要到处出现:
if ($type === 'csv') {
...
} elseif ($type === 'json') {
...
}
具体实现可以不断增加,而调用层保持相对稳定。
PHP 抽象类最常见的几个错误
第一个错误是试图直接实例化抽象类。抽象类的设计目标就是作为基础类型存在。
第二个错误是子类忘记实现抽象方法。只要子类没有完整实现父类要求的抽象方法,它就必须继续保持 abstract。
第三个错误是为了“面向对象”而到处建立抽象基类。类层次过深以后,维护成本可能比重复几行简单代码更高。
第四个错误是把接口和抽象类完全混为一谈。接口主要定义能力契约,抽象类则可以同时提供共同状态和部分实现。
第五个错误是把继承当成代码复用的唯一方式。现代 PHP 项目大量使用依赖注入和组合,很多情况下让一个类拥有另一个对象,比让它继承另一个对象更加合理。
第六个错误是把抽象类当成安全机制。abstract 主要解决的是类型和设计约束,它不会自动提高数据库安全、认证安全或者程序运行安全。
从一个实际 SaaS 项目理解抽象类
假设一个 SaaS 系统支持本地文件、Amazon S3 和 Azure Blob 三种文件存储方式,可以定义:
abstract class Storage
{
public function __construct(
protected string $bucket
) {
}
abstract public function upload(
string $file,
string $path
): bool;
abstract public function delete(
string $path
): bool;
public function normalizePath(string $path): string
{
return trim($path, '/');
}
}
S3:
class S3Storage extends Storage
{
public function upload(string $file, string $path): bool
{
$path = $this->normalizePath($path);
// S3 上传逻辑
return true;
}
public function delete(string $path): bool
{
$path = $this->normalizePath($path);
// S3 删除逻辑
return true;
}
}
Azure:
class AzureStorage extends Storage
{
public function upload(string $file, string $path): bool
{
$path = $this->normalizePath($path);
// Azure Blob 上传逻辑
return true;
}
public function delete(string $path): bool
{
$path = $this->normalizePath($path);
// Azure 删除逻辑
return true;
}
}
业务层可以依赖:
function saveFile(Storage $storage, string $file): bool
{
return $storage->upload($file, 'documents/report.pdf');
}
这样业务层不需要知道底层到底是 S3 还是 Azure。
如果未来增加本地存储或者其他对象存储,只需要增加新的具体实现。
这就是抽象类在实际项目中的核心价值:把稳定的共同部分放在基础层,把变化的实现留给具体类,同时通过 PHP 的类型系统保证继承关系不会随意偏离设计。
不过,如果不同存储实现之间没有任何共享代码和状态,只需要统一 upload()、delete() 等能力,那么接口可能比抽象类更合适。
PHP 中的 abstract 最值得掌握的并不是语法本身,而是“哪些东西应该稳定,哪些东西允许变化”这个设计问题。普通类适合完整的具体对象,接口适合定义能力契约,抽象类适合在共同基础和具体实现之间建立一个可复用的中间层,而组合和依赖注入则适合处理对象之间的协作关系。
当项目只有几个简单类时,没有必要为了显示自己掌握了面向对象而建立复杂的抽象层。到了数据库驱动、支付网关、日志系统、缓存系统、文件存储、导出器或者插件体系这类存在多个实现的场景,抽象类才会显示出价值。好的 PHP 设计并不是让继承层级越来越深,而是让变化集中在应该变化的位置,让业务代码尽可能依赖稳定的抽象,而不是绑死在某一个具体实现上。
PHP abstract 抽象类怎么使用?从继承关系理解代码设计
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP