修订面向对象的程序设计
面向对象编程不仅仅是类和对象;它是以包含数据字段和方法的对象(数据结构)为基础的整个编程范式。理解这一点非常重要;使用类将一堆不相关的方法组织在一起并不是面向对象。
假设您已经了解类(以及如何实例化类),请允许我提醒您一些不同的点和片段。
多态性
多态是一个相当长的词,但却是一个相当简单的概念。从本质上讲,多态性意味着相同的接口使用不同的底层代码。因此,多个类可以有一个绘制函数,每个函数接受相同的参数,但在底层,代码的实现是不同的。
在本节中,我想特别谈谈子类型多态性(也称为子类型化或包含多态性)。比方说,我们的超类型是动物;我们的子类型可能是猫、狗和羊。
在 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
这个脚本(同时使用 public
和 private
属性),我们将得到以下输出:
object(Bear)#1 (1) {
["growls"]=>
bool(true)
}
正如你所看到的,只有 public
属性是可见的。那么,这个小实验的寓意是什么呢?首先,var_dumps
暴露了对象内部的私有和受保护属性;其次,这种行为可以被重写。