修订面向对象的程序设计

面向对象编程不仅仅是类和对象;它是以包含数据字段和方法的对象(数据结构)为基础的整个编程范式。理解这一点非常重要;使用类将一堆不相关的方法组织在一起并不是面向对象。

假设您已经了解类(以及如何实例化类),请允许我提醒您一些不同的点和片段。

多态性

多态是一个相当长的词,但却是一个相当简单的概念。从本质上讲,多态性意味着相同的接口使用不同的底层代码。因此,多个类可以有一个绘制函数,每个函数接受相同的参数,但在底层,代码的实现是不同的。

在本节中,我想特别谈谈子类型多态性(也称为子类型化或包含多态性)。比方说,我们的超类型是动物;我们的子类型可能是猫、狗和羊。

在 PHP 中,接口允许您定义实现接口的类必须包含的一系列功能,从 PHP 7 开始,您还可以使用标量类型提示来定义我们期望的返回类型。

例如,假设我们定义了以下接口:

interface Animal
{
    public function eat(string $food) : bool;
    public function talk(bool $shout) : string;
}

然后,我们可以在自己的类中实现这个接口,如下所示:

class Cat implements Animal {
}

如果我们在没有定义类的情况下运行此代码,我们将收到如下错误消息:

Class Cat contains 2 abstract methods and must therefore be declared abstract or implement the remaining methods (Animal::eat, Animal::talk)

本质上,我们需要实现我们在接口中定义的方法,所以现在让我们继续创建一个实现这些方法的类:

class Cat implements Animal
{
    public function eat(string $food): bool
    {
        if ($food === "tuna") {
            return true;
        } else {
            return false;
        }
    }

    public function talk(bool $shout): string
    {
        if ($shout === true) {
            return "MEOW!";
        } else {
            return "Meow.";
        }
    }
}

现在我们已经实现了这些方法,接下来我们可以实例化我们所在的类并使用其中包含的函数:

$felix = new Cat();
echo $felix->talk(false);

那么,多态性从何而来呢?假设我们还有一个狗的类:

class Dog implements Animal
{
    public function eat(string $food): bool
    {
        if (($food === "dog food") || ($food === "meat")) {
            return true;
        } else {
            return false;
        }
    }

    public function talk(bool $shout): string
    {
        if ($shout === true) {
            return "WOOF!";
        } else {
            return "Woof woof.";
        }
    }
}

现在假设 pets 数组中有多种不同类型的动物:

$pets = array(
    'felix' => new Cat(),
    'oscar' => new Dog(),
    'snowflake' => new Cat()
);

现在,我们可以逐一循环所有这些宠物,以运行 talk 函数。我们并不关心宠物的类型,因为我们得到的每个类中都实现了 talk 方法,这是因为我们扩展了动物接口。

因此,假设我们想让所有动物都运行 talk 方法。我们可以使用下面的代码:

foreach ($pets as $pet) {
    echo $pet->talk(false);
}

我们不需要不必要的 switch/case 块来封装我们的类,我们只是利用软件设计来为我们的长期工作提供方便。

抽象类的工作方式与此类似,只是抽象类可以包含接口不能包含的功能。

需要注意的是,任何定义了一个或多个抽象类的类也必须定义为抽象类。不能让普通类定义抽象方法,但可以在抽象类中定义普通方法。首先,让我们将接口重构为抽象类:

Unresolved include directive in modules/ROOT/pages/ch01/ch1-02.adoc - include::example$/Chapter 1/src/Animal.php[]

你可能已经注意到,我还添加了一个 walk 方法作为普通的非抽象方法;这是一个标准方法,继承父抽象类的任何类都可以使用或扩展它。它们已经有了自己的实现。

请注意,不可能实例化抽象类(就像不可能实例化接口一样)。相反,我们必须扩展它。

因此,在我们的 Cat 类中,让我们删除以下内容:

class Cat implements Animal

我们将用以下代码取而代之:

class Cat extends Animal

这就是我们为了让类扩展 Animal 抽象类而需要重构的全部内容。我们必须在类中实现抽象函数,就像我们为接口所概述的那样,此外,我们还可以使用普通函数,而无需实现它们:

$whiskers = new Cat();
$whiskers->walk(1);

从 PHP 5.4 开始,也可以在一个系统中实例化一个类并访问其属性。PHP.net 将其宣传为:增加了实例化时访问类成员的功能,例如:(new Foo)->bar()。您也可以对单个属性进行访问,例如 (new Cat)->legs。在我们的示例中,我们可以如下使用它:

(new \IcyApril\ChapterOne\Cat())->walk(1);

在类声明或函数声明前使用 final 关键字意味着在定义了这些类或函数后就不能再覆盖它们。

因此,让我们试着扩展一个被命名为 final 的类:

final class Animal
{
    public function walk()
    {
        return "walking...";
    }
}

class Cat extends Animal
{
}

这会产生以下输出:

Fatal error: Class Cat may not inherit from final class (Animal)

同样,除了函数级别之外,让我们做同样的事情:

class Animal
{
    final public function walk()
    {
        return "walking...";
    }
}

class Cat extends Animal
{
    public function walk () {
        return "walking with tail wagging...";
    }
}

这会产生以下输出:

Fatal error: Cannot override final method Animal::walk()

Traits(多重继承)

在 PHP 中引入 Traits 是为了引入横向重用机制。PHP 传统上是一种单继承语言,因为在一个脚本中不能继承多个类。

传统的多重继承是一个有争议的过程,软件工程师们往往对此嗤之以鼻。

让我举例说明如何使用 Traits:让我们定义一个抽象的 Animal 类,并将其扩展到另一个类中:

class Animal
{
    public function walk()
    {
        return "walking...";
    }
}

class Cat extends Animal
{
    public function walk () {
        return "walking with tail wagging...";
    }
}

现在,假设我们有一个函数来命名我们的类,但我们不希望它应用于所有扩展 Animal 类的类,我们希望它应用于某些类,而不管它们是否继承了抽象 Animal 类的属性。

所以我们这样定义了我们的函数:

function setFirstName(string $name): bool
{
    $this->firstName = $name;
    return true;
}

function setLastName(string $name): bool
{
    $this->lastName = $name;
    return true;
}

现在的问题是,除了复制和粘贴不同的代码或使用条件继承外,我们没有地方可以在不使用横向重用的情况下放置这些方法。这时候,特质(Traits)就派上用场了;首先,我们将这些方法封装在一个名为 Name 的 Trait 中:

trait Name
{
    function setFirstName(string $name): bool
    {
        $this->firstName = $name;
        return true;
    }

    function setLastName(string $name): bool
    {
        $this->lastName = $name;
        return true;
    }
}

现在我们已经定义了 Trait ,只需告诉 PHP 在我们的 Cat 类中使用它即可:

class Cat extends Animal
{
    use Name;

    public function walk()
    {
        return "walking with tail wagging...";
    }
}

注意到 Name 语句的使用了吗?这就是神奇之处。现在你可以顺利调用该 Trait 中的函数了:

$whiskers = new Cat();
$whiskers->setFirstName('Paul');
echo $whiskers->firstName;

综合起来,新的代码块如下所示:

trait Name
{
    function setFirstName(string $name): bool
    {
        $this->firstName = $name;
        return true;
    }

    function setLastName(string $name): bool
    {
        $this->lastName = $name;
        return true;
    }
}

class Animal
{
    public function walk()
    {
        return "walking...";
    }
}

class Cat extends Animal
{
    use Name;

    public function walk()
    {
        return "walking with tail wagging...";
    }
}

$whiskers = new Cat();
$whiskers->setFirstName('Paul');
echo $whiskers->firstName;

标量类型提示

让我借此机会向您介绍 PHP 7 中的一个概念—​标量类型提示;它允许您定义返回类型(是的,我知道这并不严格属于 OOP 的范畴,但请接受它)。

让我们定义一个函数,如下所示:

function addNumbers (int $a, int $b): int
{
    return $a + $b;
}

让我们来看看这个函数;首先,你会注意到在每个参数之前,我们都定义了要接收的变量类型;在本例中,是 int(或整数)。接下来,你会注意到在函数定义之后有一段代码:int,它定义了我们的返回类型,因此我们的函数只能接收一个整数。

如果没有提供正确类型的变量作为函数参数,或者没有从函数中返回正确类型的变量,就会出现 TypeError 异常。在严格模式下,如果启用了严格模式,并且提供了错误的参数数,PHP 也会抛出 TypeError 异常。

在 PHP 中也可以定义 strict_types;让我来解释一下为什么要这样做。如果不定义 strict_types,PHP 将在非常有限的情况下尝试自动将变量转换为所定义的类型。例如,如果传递一个只包含数字的字符串,它将被转换为整数,但如果传递一个非数字的字符串,则会导致 TypeError 异常。一旦启用了 strict_types,这一切都会改变,你就不能再使用这种自动转换行为了。

以我们前面的例子为例,如果没有 strict_types,你可以这样做:

echo addNumbers(5, "5.0");

在启用 strict_types 后再次尝试,您会发现 PHP 引发了 TypeError 异常。

该配置仅适用于单个文件,在包含其他文件之前设置该配置不会导致这些文件继承该配置。PHP 选择这样做的原因有很多,在 0.5.3 版本的 RFC(PHP RFC:标量类型声明)中已经非常清楚地列出了实现标量类型提示的好处。您可以访问 http://www.wiki.php.net (维基,而不是 PHP 主网站)并搜索 scalar_type_hints_v5,了解相关信息。

为了启用它,请确保将其作为 PHP 脚本中的第一个语句:

declare(strict_types=1);

除非在 PHP 脚本的第一条语句中就定义了 strict_types,否则它将不起作用。事实上,如果你在后面再定义它,你的 PHP 脚本就会出现致命错误。

当然,为了那些因愤怒而阅读这本书的 PHP 核心狂热者,我应该提到,还有其他有效的类型可用于类型提示。例如,PHP 5.1.0引入了数组,而PHP 5.0.0引入了开发人员使用自己的类实现此功能的能力。

让我举一个简单的例子来说明在实践中是如何工作的:假设我们有一个 Address 类:

class Address
{
    public $firstLine;
    public $postcode;
    public $country;

    public function __construct(string $firstLine, string $postcode, string $country)
    {
        $this->firstLine = $firstLine;
        $this->postcode = $postcode;
        $this->country = $country;
    }
}

然后我们可以输入注入到 Customer 类中的 Address 类的提示:

class Customer
{
    public $name;
    public $address;

    public function __construct($name, Address $address)
    {
        $this->name = $name;
        $this->address = $address;
    }
}

这就是这一切如何结合在一起的:

$address = new Address('10 Downing Street', 'SW1A 2AA', 'UK');
$customer = new Customer('Davey Cameron', $address);
var_dump($customer);

限制对私有/受保护属性的调试访问

如果你定义了一个包含私有或受保护变量的类,那么在对该类的对象进行 var_dump 时,你会发现一种奇怪的行为。你会注意到,当你用 var_dump 封装对象时,它会显示所有变量;不管它们是受保护的、私有的还是公有的。

PHP 将 var_dump 视为内部调试函数,这意味着所有数据都会显示出来。

幸运的是,有一种解决方法。PHP 5.6 引入了 __debugInfo 魔术方法。类中以双下划线开头的函数代表魔法方法,具有与之相关的特殊功能。每次您尝试 var_dump 一个设置了 __debugInfo 魔法方法的对象时,var_dump 将被覆盖,取而代之的是该函数调用的结果。

让我向你展示一下实际操作,让我们从定义一个类开始:

class Bear {
    private $hasPaws = true;
}

让我们实例化这个类:

$richard = new Bear();

现在,如果我们尝试访问私有变量 hasPaws,我们会得到一个致命错误:

echo $richard->hasPaws;

前面的调用将导致抛出以下致命错误:

Fatal error: Cannot access private property Bear::$hasPaws

这是预期的输出,我们不希望 private 属性在其对象之外可见。话虽这么说,如果我们用 var_dump 包装对象,如下所示:

var_dump($richard);

然后我们会得到以下输出:

object(Bear)#1 (1) {
    ["hasPaws":"Bear":private]=>
    bool(true)
}

正如您所看到的,我们的 private 属性被标记为 private,但它仍然是可见的。那么我们该如何防止这种情况发生呢?

因此,让我们重新定义我们的类,如下所示:

class Bear {
    private $hasPaws = true;

    public function __debugInfo () {
        return call_user_func('get_object_vars', $this);
    }
}

现在,在实例化我们的类并 var_dump 结果对象后,我们会得到以下输出结果:

object(Bear)#1 (0) {
}

脚本现在看起来是这样的,你会注意到我额外添加了一个名为 growls 的公共属性,并将其设置为 true

<?php

class Bear {
    private $hasPaws = true;
    public $growls = true;

    public function __debugInfo () {
        return call_user_func('get_object_vars', $this);
    }
}

$richard = new Bear();
var_dump($richard);

如果我们要 var_dump 这个脚本(同时使用 publicprivate 属性),我们将得到以下输出:

object(Bear)#1 (1) {
    ["growls"]=>
    bool(true)
}

正如你所看到的,只有 public 属性是可见的。那么,这个小实验的寓意是什么呢?首先,var_dumps 暴露了对象内部的私有和受保护属性;其次,这种行为可以被重写。