通过对象组合水平扩展 PHP 类功能

引言

通过前面的学习,我们已经知道,通过类继承可以扩展类的功能,但是随着系统越来越复杂,如果仅仅通过类继承的方式扩展类的功能,而 PHP 又是单继承机制,会导致类的层级越来越深,系统也会随之变得越来越臃肿,而且新增子类来扩展功能这种方式本身也不太灵活,子类和父类相互绑定,改变父类方法会影响子类的行为,这也会让系统的可维护性大大降低。

为此,我们需要引入一种新的机制来扩展类的功能,计算机科学的先驱们早在几十年前已经开始研究这个问题,在设计模式一书中,有这样两段话,也是设计模式的两个核心原则:

1、Program to an interface, not an implementation(面向接口编程,而不是具体的实现)

2、Favor object composition over class inheritance(优先使用对象组合而不是类继承的方式实现代码复用)

关于第一点我们已经在前面介绍过了,好处也是显而易见的 —— 可以减少代码对具体实现的依赖,降低系统耦合度,关于第二点,我们接下来重点进行介绍,这也是我们解决开头提到的问题的最优解决方案,实际上,所有的设计模式也是围绕着上面两个原则展开。

基本实现

所谓对象组合,简而言之,就是在一个类中组合(或者说依赖)另一个类而不是继承另一个类来扩展它的功能,如果说类继承是垂直(纵向)扩展类功能,那么对象组合则是水平(横向)扩展类功能,从某种角度说,这也是对单继承机制缺陷的一种补充,使得类具备水平扩展功能的能力。

耳听为虚,下面我们通过一个具体的示例来演示。还是以汽车为例,传统的汽车多是通过汽油作为动力来源,随着新能源的发展和对环境保护的要求越来越高,目前行业普遍认为未来的趋势是通过电力作为汽车动力来源,并且随着特斯拉的横空出世,现在纯电动和混动汽车也越来越多,以电动汽车为主的新能源汽车行业正如火如荼地发展起来。

但是目前制约电动汽车发展的因素还很多,最核心的就是充电问题,一方面由于充电站的匮乏,充电不如加油方便,长途存在续航里程焦虑,另一方面,充电耗时数小时之久,远不及加油只需要几分钟便捷,所以作为汽车厂商,就需要做好两手准备,针对热门车型,一方面要生产油车,保证主流需求,另一方面也要生产电车,为未来做好技术储备,还可以油电混合,兼顾两者的优势,因此,体现在汽车类里面,在基本结构一致的情况下,需要支持切换不同的动力引擎,来扩展汽车的功能。

php_learning/oop 目录下新建一个 compose.php 来存放本篇教程代码,我们先将上篇教程中的 LynkCo01 类及其父类、实现接口都拷贝过来,如果要通过类继承的方式实现动力功能的扩展,需要在父类中新增相关的方法,或者让父类继承自一个包含不同动力方法的类,不管怎样都很难维护,而且代码非常臃肿,后续添加新功能或者新增动力来源代码扩展性都很差。

如果是通过对象组合的方式则非常方便和灵活,以汽油作为动力源为例,我们在 CarContract 接口中新增一个动力来源方法声明 power,然后在实现类和子类中实现这个方法:

<?php

interface CarContract
{
    public function drive();
    public function power(Gas $gas);
}

abstract class BaseCar implements CarContract
{
    protected $brand;

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

    abstract public function drive();
    abstract public function power(Gas $gas);
}

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

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

    public function power(Gas $gas)
    {
        echo "动力来源: " . $gas . PHP_EOL;
    }
}

我们通过对象组合的方式传入一个 Gas 类对象实例,就可以在目标类方法中调用该对象实例的方法组合出自己需要的功能,这里,我们只是简单打印对象实例,最后,还需要定义这个 Gas 类以及对应的魔术方法 __toString(该方法会在打印对象时调用,这样就可以将其返回结果作为打印字符串输出):

class Gas
{
    public function __toString()
    {
        return "汽油";
    }
}

编写一段测试代码:

$lynk01 = new LynkCo01();
$gas = new Gas();
$lynk01->power($gas);

执行结果是:

-w659

通过接口解除对具体类的依赖

当然,有了之前面向接口编程的经验之后,很显然上述实现与具体的实现类绑定,代码耦合度高,扩展性低,不利于后续运行时自由切换动力来源,我们通过接口类型声明来重构上述代码,先编写一组动力接口和具体的动力来源类:

interface Power
{
    public function power();
}

class Gas implements Power
{
    public function power()
    {
        return "汽油";
    }
}

class Battery implements Power
{
    public function power()
    {
        return "电池";
    }
}

然后在 LynkCo01 及其父类 BaseCar 和接口 CarContract 中移除 power 方法声明及实现,这一次,我们改为在类的构造函数参数中声明对 Power 接口实现类的依赖从而完成对象组合:

<?php

interface CarContract
{
    public function drive();
}

abstract class BaseCar implements CarContract
{
    protected $brand;
    /**
     * @var Power
     */
    protected $power;

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

    abstract public function drive();
}

class LynkCo01 extends BaseCar
{
    public function __construct(Power $power)
    {
        parent::__construct($power, '领克01');
    }

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

我们在汽车父类中新增了一个 $power 属性来持有组合对象,并且在 LynkCo01 类的构造函数中调用父类构造函数时传入 $power 对象完成 $power 属性的初始化,这样,我们就可以在当前 LynkCo01 类的其他方法中调用 $this->power 访问 $power 对象的对外可访问方法了,这里,我们在 drive() 方法中调用 $this->power->power() 打印动力来源。

编写测试代码如下:

$battery = new Battery();
$lynk01 = new LynkCo01($battery);
$lynk01->drive();
echo "电力不足,自动切换为使用汽油作为动力来源..." . PHP_EOL;
$gas = new Gas();
$lynk01 = new LynkCo01($gas);
$lynk01->drive();

上述代码的打印结果是:

-w709

以上,就是基于对象组合水平扩展 PHP 类功能的完整示例,你学会了吗?除此之外,PHP 还支持通过内置的 Trait 特性来水平扩展类功能,我们将在下篇教程中具体介绍。

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

下一篇: 通过 Trait 水平扩展 PHP 类功能