PHP 抽象类与接口(上)

引言

上篇教程学院君给大家介绍了父子类之间的继承与方法重写,并且提到类作为参数类型声明时,子类实例可以转化为父类,但父类实例不能转化为子类,这是因为,子类必然包含了父类方法,反之则不一定。

但是在实际面向对象编程实践中,并不推荐使用具体的类作为类型声明,因为当我们在声明这个类型约束时,更多考虑的是可以在对应方法中调用这个类型提供的某些方法,然后在调用该方法的地方传入的对象实例只要实现了这些方法即可,这样,该方法就不会和具体的类绑定,从而提高了代码的扩展性和复用性。

要实现类似这样的功能,就需要设计出一种新的模式,在这种模式下,定义方法参数时设定一个类型约束,然后调用该方法时,支持传入不同类的对象实例,并且需要通过某种机制保证这些类都实现了方法参数设定类型约束需要实现的方法,就好像它们之间达成了某种「契约」一样,不同的类都按照契约履行合同,而履行的方式就是实现类型约束要求提供的方法,这样一来,传入的对象实例就可以正常在方法体中使用而不会出错。

在 PHP 中,有两种方式实现这种模式,一种是抽象类,一种是接口。

抽象类

我们首先来看抽象类(Abstract Class)。

抽象类指的是包含抽象方法的类,而抽象方法是通过 abstract 关键字修饰的方法,抽象方法只是一个方法声明,不能有具体实现:

abstract public function drive();

只要某个类包含了至少一个抽象方法,它就是抽象类,抽象类也需要通过 abstract 关键字修饰(在 php_learning/oop 目录下新增一个 abstract.php 来存放本教程代码):

<?php

abstract class Car
{
    abstract public function drive();
}

如果没有通过 abstract 关键字修饰,会报错:

-w640

提示信息是,包含了抽象方法的类必须声明为抽象类。

抽象类本身不能被实例化,只能被子类继承,继承了抽象类的子类必须实现父类中的抽象方法,否则会报错:

-w717

这样一来,我们就可以基于 PHP 语法层面的约束顺利达成「契约」:将方法/函数的类型约束设置为某个抽象类,这样,传入该抽象类的子类对象就可以保证约束类型的方法被实现。

在 PhpStorm 中,可以点击错误提示下的「Add method stubs」按照向导快速添加抽象方法实现模板:

-w630

或者点击左上角的小红灯,下拉菜单也有「Add method stubs」选项:

-w525

注:可以看到上图还有一个「Make LynkCo abstract」选项,抽象类的子类如果没有实现父类的抽象方法,可以将该子类也声明为抽象类规避语法错误。

在弹出窗口选择要实现的抽象方法:

-w531

点击「OK」即可生成对应的模板代码:

class LynkCo extends Car
{
    public function drive()
    {
        // TODO: Implement drive() method.
    }
}

我们编写一段简单的实现代码:

class LynkCo01 extends Car
{
    public function drive()
    {
        echo "启动领克01汽车" . PHP_EOL;
    }
}

当然,你还可以从抽象父类继承正常的属性和方法:

<?php

abstract class Car
{
    protected $brand;

    /**
     * Car constructor.
     * @param $brand
     */
    public function __construct($brand)
    {
        $this->brand = $brand;
    }

    abstract public function drive();
}

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

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

class LynkCo03 extends Car
{
    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(Car $car)
    {
        $car->drive();
    }
}

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

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

可以看到,我们这一次在测试方法 testDrive 中设置的参数类型约束是抽象类 Car,然后在方法体中,仍然可以很方便的通过代码智能提示调用 Car 中声明的方法,在编写具体测试代码时,则传递的是子类对象,这里依据的原理仍然是子类对象可以转化为父类,只是通过抽象类声明改写之后,大大提高了代码的扩展性和优雅性,因为只要实现了该抽象类的子类都可以进行调用,该测试方法不再与具体的类绑定。

下篇教程我们来介绍 PHP 接口及其实现,抽象类实际上可以看作是面向接口编程的不完全实现,既具备了类的功能(正常的继承逻辑)又具备了接口的特性(抽象方法必须实现)。

上一篇: PHP 继承、封装与多态

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