PHP 抽象类与接口(下)

接口

说完抽象类,我们再来看接口。

和很多其他语言面向对象编程实现一样,在 PHP 中,接口也是通过 interface 关键字声明的,接口中可以定义多个方法声明,这些方法声明不能有任何实现,并且这些方法的可见性都应该是 public,因为接口中的方法都要被其他类实现。例如,我们可以通过接口方式定义 Car(在 php_learning/oop 目录下创建 interface.php 来保存本教程代码):

<?php

interface Car
{
    public function drive();
}

和抽象类的抽象方法一样,实现了某个接口的类必须实现接口声明的所有方法,否则会报错:

-w712

另外,标识一个类实现某个接口通过关键字 implements 完成,在 PhpStorm 中,要快速编写接口方法实现模板代码,和抽象方法实现模板一样,可以点击上图中「Add method stubs」,在弹出窗口选择要实现的方法:

-w567

点击「OK」就可以生成对应的方法模板了,我们在方法模板中编写简单的实现代码,并新增一个构造函数:

<?php

interface Car
{
    public function drive();
}

class LynkCo01 implements Car
{
    protected $brand;

    public function __construct($brand)
    {
        $this->brand = $brand;
    }

    public function drive()
    {
        echo "启动{$this->brand}汽车" . PHP_EOL;
    }
}

这样,就完成了对 Car 接口的实现。

接口和抽象类一样,也不能被实例化,只能被其他类实现,但是和抽象类不同,接口中不包含任何具体的属性和方法,完全是待实现的「契约」,实现接口的类就相当于和它签了契约,必须要通过实现接口中声明的所有方法来履行契约。所以我们完全可以通过接口类型定义方法中的参数类型约束,这样,就可以传入实现该接口的对象实例进行实际的方法调用,和父子类型转化原理类似,实现该接口的对象实例会被认为是该接口的实例,因为基于 PHP 的语法约束,对象所属类肯定实现了该接口的所有方法。

不过,在上述代码中,如果只有接口和具体实现类两级结构,那么所有的实现类都要定义 $brand 属性,这破坏了代码的复用性,我们可以插入一个抽象类 BaseCar 作为中间层,来定义具体实现类的共有属性,然后让抽象类实现接口,把接口方法声明为抽象方法就不需要在这一层实现,再让具体实现类继承抽象类并实现接口方法:

<?php

interface CarContract
{
    public function drive();
}

abstract class BaseCar implements CarContract
{
    protected $brand;

    public function __construct($brand)
    {
        $this->brand = $brand;
    }

    // 将接口方法声明为抽象方法,让子类去实现
    abstract public function drive();
}

class LynkCo01 extends BaseCar
{
    public function __construct()
    {
        $this->brand = '领克01';
        parent::__construct($this->brand);
    }

    public function drive()
    {
        echo "启动{$this->brand}汽车" . PHP_EOL;
    }
}

注:这里为了避免命名看起来困扰,将接口 Car 重命名为 CarContract

我们当然也可以通过一个普通的父类来定义这个 BaseCar,但是使用抽象类的好处是除了公共属性和方法这些可以被复用的代码外,对于接口中声明的方法可以直接通过抽象方法的方式抛给子类去实现,而不必在父类这一层级去实现,哪怕是空实现,这样代码结构看起来会更加优雅。

我们可以新增一个具体实现,也是实现自抽象类 BaseCar

class LynkCo03 extends BaseCar
{
    public function __construct()
    {
        $this->brand = '领克03';
        parent::__construct($this->brand);
    }

    public function drive()
    {
        echo "提示:手动档需要踩离合器" . PHP_EOL;
        echo "启动{$this->brand}汽车" . PHP_EOL;
    }
}

通过接口重构测试方法

接下来,我们就可以基于接口来重写之前的测试类和测试方法:

class TestCar
{
    public function testDrive(CarContract $car)
    {
        $car->drive();
    }
}

$lynkCo01 = new LynkCo01();
$lynkco03 = new LynkCo03();

$testCar = new TestCar();
$testCar->testDrive($lynkCo01);
echo "============================" . PHP_EOL;
$testCar->testDrive($lynkco03);

除了参数类型约束声明为了 CarContract 接口之外,其他代码没有任何变动。接口比抽象类的抽象层级更高,在代码底层设计时,使用接口更加灵活一些。在 Laravel 框架中,大量应用了 IoC 容器和依赖注入的概念,理解抽象类和接口的理念和使用,有助于后续理解 Laravel 框架的底层设计和实现。

上述代码的执行结果是:

-w560

类型运算符 instanceof

在 PHP 中,还提供了一个类型运算符 instanceof,用于判断某个对象实例是否实现了某个接口,或者是某个父类/抽象类的子类实例:

var_dump($lynkCo01 instanceof CarContract);
var_dump($lynkco03 instanceof BaseCar);

上述代码打印结果都是 bool(true),表示 LynkCo03 实现了 CarContract 接口也继承了 BaseCar 基类,而由于 TestCar 两者都不满足,所以下面的代码打印结果都是 bool(false)

var_dump($testCar instanceof CarContract);
var_dump($testCar instanceof BaseCar);

关于接口和抽象类,学院君就简单介绍到这里,更多细节,请参考官方文档

上一篇: PHP 抽象类与接口(上)

下一篇: 通过对象组合水平扩展 PHP 类功能