PHP 继承、封装与多态

上篇教程学院君给大家介绍了 PHP 面向对象编程中的基本概念 —— 类与对象,今天我们在此基础上来看看面向对象的三大特性:继承、封装与多态。

继承

所谓继承,指的是子类可以通过继承的方式访问父类的属性和方法(protected 或者 public 方式定义),在 PHP 中,继承通过 extends 关键字实现,我们以上篇教程编写的 Car 类为例,编写一个实现该类的子类 Benz(仍然在 class.php 中定义):

class Benz extends Car
{
    public function __construct($seats = 5, $doors = 4, $engine = 1)
    {
        $this->brand = '奔驰';
        // $this->setBrand('奔驰');  // 也可以通过该方法设置
        parent::__construct($this->brand, $seats, $doors, $engine);
    }
}

这里 extends Car 的含义就是 Benz 继承自 Car,是它的子类,相对的,CarBenz 的父类。

在子类 Benz 的构造函数中,我们将品牌设置为「奔驰」,然后通过 parent::__construct 调用父类的构造函数进行初始化(调用父类的同名方法需要通过 parent:: 进行调用,否则 PHP 会不知道调用父类还是子类的方法),这样,初始化 Benz 对象时,就无须传入品牌参数了。

可以看到,在子类中可以通过 $this 对象直接访问父类定义的属性和方法,前提是该属性或方法的可见性是 protected 或者 public 级别,如果试图访问 private 声明的属性或方法,PhpStorm 会警告:

-w673

运行代码也会报错。

另外,我们也可以通过子类对象访问父类方法(在子类函数体中访问父类方法,通过 $this 即可):

$benz = new Benz();
$benz->drive();

上述代码的执行结果如下:

-w604

可以看到子类可以继承父类所有通过 protectedpublic 声明的属性和方法,并且在调用过程中自动将 $this 指针引用指向子类对象,对于 public 属性和方法,和父类一样,直接可以在类外部通过 -> 操作符调用。

当然,你也可以在子类中新增一些独有的属性和方法:

class Benz extends Car
{
    private $customProp = "自定义属性";
    
    ...

    public function customMethod()
    {
        echo "Call custom prop \$customProp: " . $this->customProp . PHP_EOL;
        echo "This is a custom method in Benz Class" . PHP_EOL;
    }
}

PHP 遵循单继承机制,即一个子类只能继承自一个父类。

封装

概念解释

封装一方面指的是调用者无需关心对象方法实现细节,比如我们要开车,就调用 $car->drive() 方法即可,不用编写具体的实现逻辑,也不用去关心(调用了那些属性、那些方法、不管是私有的还是公开的、当前类的还是其他类的,统统不用关心),就像我们在真实世界中开车一样,只需要按照流程来操作就好了,不用关心汽车引擎内部是如何工作的。

另一方面是通过访问控制限定属性和方法的可见性,比如 public 修饰的属性和方法所有地方可见,不管是当前类、子类还是类之外,protected 修饰的属性和方法在当前类和子类中可见,而 private 修饰的属性和方法仅在当前类可见,你可以根据自己的业务需要合理的设置属性和方法的可见性。

反射

不过,饶是如此,依然可以通过反射的方式将 protected 或者 private 级别的属性和方法变成类以外可以访问,比如我们将 Benz 类中的 customMethod 方法设置为私有的:

class Benz extends Car
{
    private $customProp = '自定义属性';

    ...

    private function customMethod()
    {
        echo "Call custom prop \$customProp: " . $this->customProp . PHP_EOL;
        echo "This is a custom method in Benz Class" . PHP_EOL;
    }
}

在类外直接调用会报错:

-w655

我们通过反射来调用这个方法,可以这么做:

// 通过反射调用非 public 方法
$method = new ReflectionMethod(Benz::class, 'customMethod');
$method->setAccessible(true);

$benz = new Benz();
$method->invoke($benz);

打印结果和调用声明为 publiccustomMethod 方法完全一样,如果将 private 修改为 protected 效果也一样,通过反射,我们可以在运行时以逆向工程的方式对 PHP 类进行实例化,并对类中的属性和方法进行动态调用,不管这些属性和方法是否对外公开,所以这是一个黑科技,更多反射的细节可以参考 PHP 官方文档:https://www.php.net/manual/zh/book.reflection.php

多态

方法重写

所谓多态,指的是在 PHP 继承体系中,子类可以重写父类的同名方法,这样,在子类对象中调用该方法,就会自动转发到子类方法调用,还是以 CarBenz 为例,我们在子类中重写父类的 drive 方法(所谓重写,英文是 override,即在子类中编写和父类同名方法,来覆盖父类的实现):

class Benz extends Car
{
    ...

    // 重写父类实现
    public function drive()
    {
        echo $this->getBrand() . '汽车的启动流程:' . PHP_EOL;
        parent::drive(); // TODO: Change the autogenerated stub
    }
}

我们在子类的 drive 方法中,先打印了一段提示文本,然后和构造函数一样,通过 parent::drive 调用父类的同名方法,因为所有的汽车启动流程基本都是一样的。

接下来,我们通过子类对象调用 drive 方法:

$benz = new Benz();
$benz->drive();

打印结果如下:

-w581

包含了第一行提示文本,所以,调用的是子类的方法而不是父类的。

类型转化

PHP 不像 Java 那样支持同一个类中定义多个同名方法(参数数量或类型不同,这种叫做方法重载),另外,由于子类一定包含了父类的公开方法,所以当类作为参数类型声明时,如果声明类型为父类,则可以传入子类对象,反过来,如果声明类型为子类,则不能传入父类对象。

比如我们定义一个测试汽车类启动功能的测试类和方法如下,并编写一段测试代码:

class TestCarDrive
{
    public function testDrive(Car $car)
    {
        $car->drive();
    }

    public function testBenzDrive(Benz $benz)
    {
        $benz->drive();
    }
}

// 初始化类对象
$bmw = new Car('宝马');
$benz = new Benz();
$test = new TestCarDrive();

// 测试子类转父类
$test->testDrive($benz);
// 测试父类转子类
$test->testBenzDrive($bmw);

上述代码第一个测试 $test->testDrive 可以正常运行,第二个会报错:

-w1380

错误提示时不能将父类对象转化为子类对象,因为存在方法不兼容。在此基础上,可以进一步抽象声明的参数类型,进而引申出抽象类和接口的概念,我们将在下一篇教程介绍它们。

上一篇: PHP 类与对象、访问控制

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