揭秘多态性——接口和抽象类

就责任分离而言,事件分派已经是一个先进的概念。如果你知道、了解并有机会使用这种机制,你就可以认为自己在简洁代码世界中的水平已经大大提高了。所有这些显然都需要一些设置。要么自己实现这个系统,要么使用外部库。在第二种情况下,显然需要一个完整的学习阶段。无论如何,这显然都不是改进职责分离的唯一方法。还有一种 PHP 原生的方法可以改善责任分离,这种方法有时没有被充分使用,有时被误解,还经常被低估。我们在这里谈论的是多态性,或者通俗地说:抽象类和接口。

首先,为什么用多态这个词?Poly 来自希腊语,意思是很多,而 morphism 的意思是形式/形状。抽象类和接口只是在代码和面向对象编程(OOP)中实现多态性的一种方法。为简化起见,我们仅以接口为例。

接口

接口为以后实现接口的类定义了共同的形式/形状。它们决定了每个实现应为其情况定义的方法。一个实现必须定义其接口的所有方法。这就是我们经常听到以下说法的原因: "接口就是合同"。我们可以这样理解:如果你实现了一个接口,你就承诺要实现它所定义的方法。你别无选择。

这就是多态性的全部威力所在。在你的代码中,你可以告诉 PHP,一个方法的某某参数必然是一个实现了精确接口的对象的实例。你可以操作这个接口的不同方法并调用它们。你不必担心在使用时会如何实现:我们对此不感兴趣。一个例子胜过千言万语,让我们以邮件系统为例。

MailerInterface 只定义了一个方法:发送邮件的方法。我们可以将其命名为 sendEmail。和以前一样,当用户在应用程序中被删除时,发送告别邮件的事件监听器就会被调用。在这种情况下,你感兴趣的是邮件是否被发送,而不是发送的内部运作。顺便问一下,这些内部工作难道不能根据某些条件而有所不同吗?举个例子:你的主要电子邮件提供商可能宕机了,但你绝对需要发送邮件。这时,你就必须使用另一个电子邮件提供商,它有不同的应用程序编程接口(API)和不同的选项,等等。如果没有多态性,事情很快就会变得非常复杂。

解决方案是创建 MailerInterface 的两个实现,每个实现都定义了 sendEmail 方法,具体取决于使用的电子邮件提供商。但结果是一样的:邮件已发送。当用户删除自己的账户时,我们会进行检查,确保主要电子邮件提供商正常运行,并将其实现实例化。如果主电子邮件提供商宕机,则实例化备用电子邮件提供商的实现。另一方面,在电子邮件发送事件监听器中,你只需继续调用 MailerInterface 中定义的 sendEmail 方法,而无需担心其它问题。代码简洁明了,责任分明,节省时间。除此之外,它还能抵御失败。

如果你愿意,你可以用 10、15 或 20 个电子邮件提供商来做这件事。这样做的好处是,如果其中一个提供商的应用程序接口(API)发生了变化,或者你在实施过程中发现了一个错误,你只需要触动有问题的那个实施方案。其它的都不会改变,就像对接口的调用一样。这样就大大降低了出现错误的风险,而且代码的可测试性也大大提高:你可以专门为每个实现编写测试。这比试图评估每种可能情况的泛化的、无休止的测试要强大得多!节省下来的时间是无价的。

抽象类

那么抽象类又是如何融入这一切的呢?我们可以将抽象类视为接口与其实现之间的中间层。虽然抽象类实现接口显然不是强制性的,但在抽象类之上创建接口往往是一个聪明的想法。事实上,抽象类比接口更自由:你可以部分地定义所声明的方法、声明属性,并决定类的方法和属性的可见性(而接口只允许公开可见,不允许私有或受保护)。如果遵守 SOLID 原则的 "I"--接口隔离—​那么有了接口,你就有了一个简洁的契约,没有任何 "以防万一 "的信息,只有最基本的信息。作为提醒,简单地说,该原则表明接口不应包含 "以防万一 "声明的方法,每个方法都应有助于满足单一责任原则。

抽象类允许我们为扩展抽象类并希望利用多态性功能的类定义共同的行为。这尤其可以避免代码冗余、错误源和无休止的复制粘贴。事实上,在我们前面的例子中,MailerInterface 的不同实现极有可能具有共同的行为,例如创建与电子邮件提供商的 API 通信的 HTTP 客户端,或创建在实现的内部运作中使用的 Message 公共对象。

在这种情况下,我们将声明实现 MailerInterface 的 AbstractMailer,并定义不同实现的共同行为。然后,不同的实现将扩展 AbstractMailer,以享受你刚刚定义的共同行为。

请注意,这并不意味着必须在所有地方、所有时间和所有情况下创建接口和抽象类。与单个类相比,我们不应忽视这对代码复杂性的影响。此外,没有为某个案例创建接口并不意味着它是不可改变的、一成不变的。很多时候,我们会发现自己在重构代码,创建接口和抽象类,并调整现有的类来实现和扩展它们。正如我们所看到的,一开始我们需要保持代码简单(遵守 YAGNI 和 KISS 原则)。我们无法预测未来,业务约束会不断变化。

如果在创建一个类时,没有任何迹象表明需要不同的实现方式,那就不用担心。这是以后要做的工作。另一方面,如果在开发过程中,你发现自己从一边复制代码到另一边,并感觉到有很强的冗余性,那么考虑多态性将是一个很好的条件反射。

总结

我们刚刚学习了本书理论部分中最先进的部分。现在,我们已经掌握了一些知识,可以在保持代码可维护性和可扩展性的同时,为未来的开发人员干净利落地剪切代码。此外,它还将通过对扩展的强烈开放性和对修改的封闭性(如 SOLID 原则之一所述)为未来做好准备。

我们回顾了在开发 PHP 应用程序时可能遇到的有关文件、类和方法命名的许多情况。此外,我们还了解到文件夹必须有特定的名称,并可用于将应用程序划分为不同的域。

责任分离也是一个重要的话题。尤其重要的是,要理解为什么这种职责分离在项目中是有用的,甚至是至关重要的。它是一个架构良好、易于操作的项目的真正关键。正如我们所见,事件分派是实现这一目标的绝佳方式。事件分派是一些关键网络项目的基石之一,例如 Symfony 框架。该框架在很大程度上依赖于这种机制,使其成为一种以健壮性、高效性,尤其是灵活性而著称的工具。这也要归功于多态性和其中声明的不同接口。有了它,你可以重新声明框架的几乎所有部分,使一切都能适应你最先进的需求。

要理解何时创建接口或抽象类并非易事。这需要实践和经验。很快,它就会显得很自然。如有疑问,请与同行交流!

在本书理论部分的最后,我们将在下一章讨论 PHP 的新特性。尤其是最近几年,这些新特性使我们的开发工作更加严谨,开发人员的能力也得到了提高。