面向接口编程


引言

在编程中,确保你的代码可读性、可维护性、可扩展性和易于测试是非常重要的。我们可以通过使用接口来提高我们代码的所有这些因素。

目标读者

相比于上篇教程,这篇教程的内容看起来可能会稍微有些吓人。本章针对的是具有 OOP(面向对象编程)基本概念和 PHP 继承使用经验的开发人员。如果你知道如何在你的 PHP 代码中使用继承,这篇教程应该会让你理解起来没那么困难。

什么是接口?

简单来说,接口就是对类应做什么的描述。它们可以用来确保实现接口的任何类都包含在其中定义的每个公共方法。

接口可以用来:

  • 定义类的公共方法。
  • 定义类的常量。

接口不能:

  • 自行实例化。
  • 定义类的私有或受保护方法。
  • 定义类的属性。

接口用于定义类应包含的公共方法。重要的是要记住,只有方法签名是被定义的,不包括方法主体(你通常会在类的方法中看到的)。这是因为接口只用于定义对象之间的通信,而不是像在类中那样定义通信和行为。要把这个点置于一些具体的背景中,这个例子显示了一个定义了几个公共方法的例子接口:

image-20230831165031446

根据 php.net,接口有两个主要的目的:

  1. 允许开发者创建各种实现了相同接口或接口的不同类的对象,这些对象可以互换使用。常见的例子是多个数据库访问服务,多个付款网关,或者不同的缓存策略。不同的实现可以在不需要更改使用它们的代码的情况下进行替换。
  2. 允许一个函数或方法接收并操作符合接口的参数,而不关心对象可能做什么或者如何实现。这些接口通常被命名为 Iterable、Cacheable、Renderable 等,来描述行为的重要性。

在 PHP 中使用接口

接口可以是 OOP(面向对象编程)代码库中的极其重要的一部分。它们允许我们解耦我们的代码并提高扩展性。为了说明这一点,让我们看看下面这个类:

image-20230831165246638

如你所见,我们已经定义了一个类,里面有一个返回字符串的方法。通过这样做,我们定义了方法的行为,所以我们可以看到 getName() 是如何构建返回的字符串的。

然而,假设我们在代码里的另一个类中调用这个方法。另一个类并不关心这个字符串是如何构建的,它只关心这个字符串是否被返回。比如,让我们看看我们如何在另一个类中调用这个方法:

image-20230831165317902

虽然上面的代码可以工作,但是假设我们现在想要添加下载包裹在 UsersReport 类中的用户报告的功能。当然,我们不能在我们的 ReportDownloadService 中使用现有的方法,因为我们已经强制只能传递 BlogReport 类。所以,我们将不得不重命名现有的方法,然后添加一个新的方法,如下:

image-20230831165401554

虽然你其实看不到,但假设上面的类中的其他方法使用的代码是相同的,我们可以把共享代码提出来做成方法,但我们仍然可能会有一些共享代码。而且,我们会有多个入口点进入几乎运行相同代码的类。这可能导致在未来尝试扩展代码或添加测试时需要做额外的工作。

例如,假设我们创建一个新的 AnalyticsReport;我们现在需要在类中添加一个新的downloadAnalyticsReportPDF() 方法。你可能可以看到这个文件如何可能开始迅速增长。这可能是使用接口的一个完美的场所!

让我们开始创建一个;我们将其命名为 DownloadableReport 并像这样定义它:

image-20230831165458260

我们现在可以如下面的例子那样更新 BlogReportUsersReport,使其实现 DownloadableReport 接口。但请注意,我特意写了一个错误的 UsersReport 代码,以便我可以演示一些事情!

image-20230831165520920

image-20230831165532958

如果我们试图运行我们的代码,我们将因为以下原因获得错误:

  1. getHeaders() 方法缺失。
  2. getName() 方法没有包含在接口的方法签名中定义的返回类型。
  3. getData() 方法定义了一个返回类型,但它并不是在接口的方法签名中定义的那个。

所以,要更新 UsersReport,使其正确实现 DownloadableReport 接口,我们可以把它改为以下的:

image-20230831165623604

现在我们有两个报告类实现相同的接口,我们可以像这样更新我们的 ReportDownloadService

image-20230831165638590

我们可以在 downloadReportPDF() 方法中传入 UsersReportBlogReport 对象,而不会出现任何错误。这是因为我们现在知道报告类中需要的方法存在并且返回我们期望的数据类型。

由于我们在方法中传入了一个接口而不是一个类,这使我们能够根据方法的任务而不是方法的执行方式,松散地将 ReportDownloadService 和报告类链接起来。

如果我们想创建一个新的 AnalyticsReport,我们可以让它实现同一个接口,然后这将允许我们将报告对象传入同一个 downloadReportPDF() 方法,而不需要添加任何新的方法。这在你正在制作你自己的包或框架,并希望让开发者创建他们自己的类时,可能会特别有用。你只需要告诉他们需要实现哪个接口,他们就可以创建他们自己的新类。例如,你可以在 Laravel 中,通过实现 Illuminate\Contracts\Cache\Store 接口创建你自己的自定义缓存驱动类。

除了使用接口来改善实际的代码,我倾向于喜欢接口,因为它们可以充当代码即文档。例如,如果我试图找出一个类能做什么、不能做什么,我倾向于首先查看接口,而不是使用它的类。它告诉你所有可以调用的方法,而不需要我过多了解方法在底层是如何运行的。

对于任何阅读我文章的 Laravel 开发者来说,值得注意的是你将会经常看到 "contract" 和 "interface" 这两个术语被互相替换使用。根据 Laravel 的文档,“Laravel 的 contracts 是一组定义了框架提供的核心服务的接口”。所以,重要的是要记住,contract 就是一个接口,但接口不一定是 contract。通常,contract 只是框架提供的接口。如果你想要更多关于使用 contracts 的信息,我建议你去阅读文档,我认为它做得很好,能够让你理解它们是什么,如何使用它们,何时使用它们。

小结

这篇教程让你了解了什么是接口,它们在 PHP 中如何被使用,以及使用它们的好处。


点赞 取消点赞 收藏 取消收藏

<< 上一篇: 辅助函数篇

>> 下一篇: 使用策略模式