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 继承。
  • 类可以实现多个接口,但只能继承一个抽象类。
  • 接口强调 特定功能的实现,而抽象类强调 所属关系
  • 接口中定义的所有方法必须在实现类中实现。抽象类可以包含普通方法,并且只要求实现所有抽象方法。
  • 接口中的方法默认是公有的,并且只能声明为公有的。抽象类的抽象方法可以声明为公有的或受保护的。