1. 前言
PHP 支持抽象类(Abstract Class)和接口(Interface),合理地使用它们,对增强代码的扩展性和可维护性都有很大的帮助。那么什么时候使用抽象类,什么时候使用接口,以及它们之间有哪些区别呢?今天我们来详细整理下。
2. 概念
2.1 抽象类
定义
PHP 5 引入了抽象类和抽象方法。抽象类不能被实例化,并且一个类中只要包含有一个抽象方法,就必须被声明为抽象类。抽象方法只能定义方法的参数——不能定义具体实现。
继承抽象类时,子类必须定义父类中的所有抽象方法;此外,这些方法的可见性必须和父类相同(或者比父类更宽松)。例如,如果抽象方法被定义为受保护的,那么此方法的实现必须被定义为受保护的或公有的,而不能是私有的。此外,方法的参数也必须匹配,例如参数类型提示和必需参数的个数必须一致。举例来说,如果子类定义了一个父类抽象方法中没有的可选参数,那么两者并无冲突。这也适用于构造函数。
示例
#1 抽象类示例
abstract class AbstractClass
{
// 强制要求子类定义这些方法
abstract protected function getValue();
abstract protected function prefixValue($prefix);
// 普通方法
public function printOut()
{
print $this->getValue() . "\n";
}
}
class ConcreteClass1 extends AbstractClass
{
protected function getValue()
{
return 'ConcreteClass1';
}
public function prefixValue($prefix)
{
return "{$prefix}ConcreteClass1";
}
}
class ConcreteClass2 extends AbstractClass
{
protected function getValue()
{
return 'ConcreteClass2';
}
public function prefixValue($prefix)
{
return "{$prefix}ConcreteClass2";
}
}
$class1 = new ConcreteClass1();
$class1->printOut();
echo $class1->prefixValue('FOO_') . "\n";
$class2 = new ConcreteClass2();
$class2->printOut();
echo $class2->prefixValue('FOO_') . "\n";
/* 结果
ConcreteClass1
FOO_ConcreteClass1
ConcreteClass2
FOO_ConcreteClass2
*/
#2 抽象类示例
abstract class AbstractClass
{
// 抽象方法只需要定义必需参数
abstract protected function prefixName($name);
}
class ConcreteClass extends AbstractClass
{
// 子类中可以定义父类参数中不存在的可选参数
public function prefixName($name, $separator = '.')
{
if ($name === 'Pacman') {
$prefix = 'Mr';
} elseif ($name === 'Pacwoman') {
$prefix = 'Mrs';
} else {
$prefix = '';
}
return "{$prefix}{$separator} {$name}";
}
}
$class = new ConcreteClass();
echo $class->prefixName('Pacman'), "\n";
echo $class->prefixName('Pacwoman'), "\n";
/* 输出
Mr. Pacman
Mrs. Pacwoman
*/
2.2 接口
定义
使用接口,可以指定创建类时必须实现的方法,而不需要定义这些方法是怎样实现的。
接口和类采用相同的方式定义,但是使用 interface
关键字代替 class
关键字,并且方法中不定义任何内容。
接口中定义的所有方法都必须是公有的;这是接口的特性。
需要注意的是,可以在接口中声明构造函数,这在一些上下文环境中会很有用,例如通过工厂使用。
实现
要实现一个接口,使用 implements
操作符。在类中必须实现接口的所有方法;否则会导致致命错误。类可以实现多个接口,用逗号分隔每个接口。
在 PHP 5.3.9 之前,类无法实现两个指定了相同方法名的接口,因为这会导致歧义。更新版本的 PHP 允许这样做,只要重复的方法有相同的参数。
接口也可以使用
extends
操作符继承。类要实现接口,必须使用与接口中所定义的相同的方法参数。否则,会导致致命错误。
常量
接口中可以定义常量。接口常量和类常量的使用完全相同,但是不能被子类或子接口重写。
示例
#1 接口示例
// 声明接口 iTemplate
interface iTemplate
{
public function setVariable($name, $var);
public function getHtml($template);
}
// 实现接口
// 正确写法
class Template implements iTemplate
{
private $vars = [];
public function setVariable($name, $var)
{
$this->vars[$name] = $var;
}
public function getHtml($template)
{
foreach ($this->vars as $name => $value) {
$template = str_replace('{' . $name . '}', $value, $template);
}
return $template;
}
}
// 错误写法
// 致命错误:Class BadTemplate contains 1 abstract methods
// and must therefore be declared abstract (iTemplate::getHtml)
class BadTemplate implements iTemplate
{
private $vars = [];
public function setVariable($name, $var)
{
$this->vars[$name] = $var;
}
}
#2 接口继承
interface a
{
public function foo();
}
interface b extends a {
public function baz(Baz $baz);
}
// 正确写法
class c implements b
{
public function foo()
{
}
public function baz(Baz $baz)
{
}
}
// 错误写法,会导致致命错误
class d implements b {
public function foo()
{
}
public function baz(Foo $foo)
{
}
}
#3 继承多个接口
interface a
{
public function foo();
}
interface b
{
public function bar();
}
interface c extends a, b
{
public function baz();
}
class d implements c
{
public function foo()
{
}
public function bar()
{
}
public function baz()
{
}
}
3. 区别
相同点:
- 两者都是抽象出来的类,都不能实例化。因此也不能声明成员变量,但可以声明常量。
- 接口的实现类和抽象类的子类都必须实现声明的抽象方法。
不同点:
- 接口使用
implements
实现,抽象类使用extends
继承。 - 类可以实现多个接口,但只能继承一个抽象类。
- 接口强调 特定功能的实现,而抽象类强调 所属关系。
- 接口中定义的所有方法必须在实现类中实现。抽象类可以包含普通方法,并且只要求实现所有抽象方法。
- 接口中的方法默认是公有的,并且只能声明为公有的。抽象类的抽象方法可以声明为公有的或受保护的。