在PHP面向对象编程里,抽象类和接口这两个概念经常被放在一起比较。它们都能为类提供结构上的规范,但二者的设计哲学和应用场景有着本质的区别。一句话先帮你建立直觉印象:抽象类是有血有肉的半成品,接口是一张只有要求没有实现的设计图。
把这两个东西搞清楚,你的代码架构能力会迈上一个台阶。
抽象类:带默认实现的半成品
抽象类本质上还是一个类,它用 abstract 关键词声明,不能直接实例化。但和接口不同,抽象类里可以既有抽象方法(只有签名没有函数体),也有普通方法(有完整实现)。抽象类还可以拥有属性、构造方法,并且支持 public、protected、private 三种访问修饰符。
它适合这样的场景:你预见到会有一系列子类共享大量相同的字段和方法逻辑,但其中某些行为必须由每个子类自己去定义。
抽象类的基础语法
abstract class ParentClass {
abstract public function myMethod1();
abstract public function myMethod2($name, $age);
abstract public function myMethod3(): string;
}
注意,所有抽象方法前面都必须加上 abstract 关键词,而且一个类只要包含了至少一个抽象方法,这个类本身就必须声明为抽象类。
乐器示例:抽象类在行动
下面用乐器来演示一下抽象类的实际用法。所有乐器都有一个名字,也都能发出声音,但每种乐器的发声方式不同。Instrument 抽象类承担共享数据和共同行为,而 sound() 则作为抽象方法留给子类各自发挥:
abstract class Instrument {
public $name;
public function __construct($name) {
$this->name = $name;
}
abstract public function sound(): string;
}
class Guitar extends Instrument {
public function sound(): string {
return "Strumming strings! I'm a $this->name.";
}
}
class Piano extends Instrument {
public function sound(): string {
return "Melodic keys! I'm a $this->name.";
}
}
class Drums extends Instrument {
public function sound(): string {
return "Loud and rhythmic! I'm a $this->name.";
}
}
$guitar = new Guitar("Guitar");
echo $guitar->sound();
echo "<br>";
$piano = new Piano("Piano");
echo $piano->sound();
echo "<br>";
$drums = new Drums("Drums");
echo $drums->sound();
输出:
Strumming strings! I'm a Guitar.
Melodic keys! I'm a Piano.
Loud and rhythmic! I'm a Drums.
从这个例子可以明显看出抽象类的核心价值:$name 属性和 __construct() 构造方法放在父类里写一次就够了,三个子类自动拥有,不用重复声明。而 sound() 方法虽然在父类里定义了规范(必须返回 string),但具体内容被推迟到子类。这就是“部分抽象”——共享的部分写死,个性的部分强制子类填充。
抽象类的关键特征
-
可以同时包含抽象方法和非抽象方法(部分抽象)
-
可以声明属性,有自己的构造方法
-
支持
public、protected、private访问修饰符 -
使用
extends关键词继承 -
不能直接实例化
接口:一份纯粹的能力清单
接口比抽象类走得更远,它追求的是抽象——接口里不能有任何方法实现,也不能声明属性。接口里的所有方法自动为 public,也只能是 public。它的职责只有一个:定义一套方法签名,强制实现类去遵守。
接口用 interface 关键词定义,用 implements 关键词来被类实现。一个类可以同时实现多个接口,这是PHP绕过单继承限制的主要手段。
接口的基础语法
interface MyInterfaceName {
public function methodA();
public function methodB();
}
几何图形示例:接口的典型应用
还是用 Shape 接口来展示。它声明了两个方法:area() 和 getDimensions(),不关心面积怎么算出来的:
interface Shape {
public function area(): float;
public function getDimensions(): string;
}
class implements Shape {
private $base;
private $height;
public function __construct($base, $height) {
$this->base = $base;
$this->height = $height;
}
public function area(): float {
return 0.5 * $this->base * $this->height;
}
public function getDimensions(): string {
return "Base: {$this->base}, Height: {$this->height}";
}
}
class Rectangle implements Shape {
private $length;
private $width;
public function __construct($length, $width) {
$this->length = $length;
$this->width = $width;
}
public function area(): float {
return $this->length * $this->width;
}
public function getDimensions(): string {
return "Length: {$this->length}, Width: {$this->width}";
}
}
$ = new (5, 10);
echo $->getDimensions() . " | Area of : " . $->area() . PHP_EOL;
$rectangle = new Rectangle(4, 6);
echo $rectangle->getDimensions() . " | Area of Rectangle: " . $rectangle->area() . PHP_EOL;
输出:
Base: 5, Height: 10 | Area of : 25
Length: 4, Width: 6 | Area of Rectangle: 24
这里 和 Rectangle 之间没有继承关系,但它们因为都实现了 Shape 接口而被纳入了同一个“形状生态”。接口的本质是能力契约,而不是血缘纽带。
接口的关键特征
-
只包含方法声明,没有任何方法体(抽象)
-
不能声明变量,但可以定义常量
-
所有方法默认为
public,不允许其他访问修饰符 -
一个类可以同时实现多个接口,用逗号分隔
-
使用
implements关键词实现
抽象类 vs 接口:一张表看懂区别
这个对照表值得你多看几遍,它把二者的差异做了系统梳理:
| 特性 | 抽象类 | 接口 |
|---|---|---|
| 抽象程度 | 部分抽象 | 抽象 |
| 方法类型 | 可同时包含抽象方法和普通方法 | 只能有抽象方法 |
| 变量声明 | 可以声明属性 | 不能声明变量(可声明常量) |
| 访问修饰符 | 支持 public、protected、private |
仅支持 public |
| 继承/实现关键词 | extends |
implements |
| 多继承 | 不支持 | 支持(实现多个接口) |
| 代码复用 | 可通过非抽象方法实现在码复用 | 不提供任何代码复用 |
场景抉择:什么时候选谁?
很多人在做设计时会在抽象类和接口之间犹豫不决。我个人的判断逻辑是这样的——你先问自己一个问题:这些类之间,有没有可以被共享的数据和默认行为?
选抽象类的时机
-
你的几个类确实属于同一个“家族”,有清晰的层级关系
-
有一些属性和方法是所有子类共有的,适合抽到父类里避免重复
-
你需要
protected或private的成员来控制访问权限 -
你想为某些方法提供默认实现,子类可以按需覆盖
乐器那个例子就是抽象类的典型应用场——吉他、钢琴、鼓都“是一种”乐器,共享 $name 属性和构造方法,sound() 则留给子类各自实现。
选接口的时机
-
你需要定义一组跨类别的通用能力,实现类之间可能毫无血缘关系
-
你想达到类似多继承的效果,让一个类具备多种独立的行为
-
你只需要公开的方法声明,不需要任何实现细节
-
你想设计松耦合、可插拔的组件
几何图形的例子正好切题——三角形和矩形之间没有任何继承关系,但通过实现同一个 Shape 接口,它们被统一纳入了面积计算的规范体系。更典型的还有PHP中内置的 Countable、Iterator 等接口,实现它们的类可能不搭边,但因为都遵守了同一套契约,所以可以用 count() 函数或 foreach 循环统一处理。
有时候两者可以配合使用
项目开发中不需要非此即彼。我就经常这么干:先写一个抽象类把共享的属性和方法抽取出来,同时让它实现某个接口,从而既有代码复用,又享受了接口的契约规范。子类继承抽象类后,也就自动成为接口的实现者之一,这是PHP面向对象设计里比较灵活的一种组合策略。
本节课程知识要点
-
抽象类是部分抽象的,可包含抽象方法和普通方法,支持属性声明和各类访问修饰符,用
extends继承。 -
接口是抽象的,只有方法签名没有实现,不声明变量,方法强制为
public,用implements实现。 -
PHP 类只能继承一个父类,但可以同时实现多个接口,这是绕过单继承限制的主要途径。
-
选择抽象类的核心判断依据是 “有共享数据和默认行为”,选择接口的判断依据是 “需要统一的能力契约”。
-
二者不是互斥关系,可以根据场景灵活搭配使用。