什么是接口?如何在PHP中使用接口?本篇文章带大家聊聊使用接口编写更优雅的 PHP 代码,希望对大家有所帮助!
在编程中,确保代码可读、可维护、可扩展和易于测试是很重要的;而使用接口,恰恰是我们改进代码中所有这些因素的方法之一。
目标读者
本文的目标读者是对 OOP(面向对象编程)概念有基本了解并在 PHP 中使用继承的开发人员。如果你知道如何在 PHP 代码中使用继承,那么你应该可以很好地理解本文。
什么是接口?
简而言之,接口只是对类应该做什么的描述,它们可用于确保实现该接口的任何类都将包括在其内部定义的每个公共方法。
接口可以:
- 用于定义类的公共方法;
- 用于定义类的常量。
接口不可以:
- 被实例化;
- 用于定义类的私有或受保护方法;
- 用于定义类的属性。
接口是用来定义一个类应该包括的公共方法的。记住,你只需要在接口里定义方法的签名,而不需要包含方法的主体(就像通常在类中看到的方法一样)。**这是因为接口仅用于定义对象之间的通信,而不是像在类中那样定义通信和行为。**为了说明这个问题,下面展示了一个定义了几个公共方法的示例接口:
interface DownloadableReport { public function getName(): string; public function getHeaders(): array; public function getData(): array; }
根据 php.net 文档我们可以知道,接口有两个主要用途:
-
允许开发者创建不同类别的对象,这些对象可以互换使用,因为它们实现了相同的一个或多个接口。常见的例子包含:多个数据库访问服务、多个支付网关、不同的缓存策略等。不同的实现之间可以互换,而不需要对使用它们的代码进行任何修改。
-
允许函数或方法接受符合接口的参数并对其进行操作,而不关心该对象还可以做什么或它是如何实现的。这些接口通常被命名为
Iterable
、Cacheable
、Renderable
等,来说明这些接口的实际含义。
在 PHP 中使用接口
接口是 OOP(面向对象编程)代码库的重要部分。接口能让我们降低代码耦合并提高可扩展性。举个例子,让我们看看下面这个类:
class BlogReport { public function getName(): string { return 'Blog report'; } }
如你所见,我们定义了一个类,类中有一个函数,返回一个字符串。这样一来,我们定义了该方法的行为,所以我们知道 getName()
是如何返回字符串的。不过,假设我们在另一个类调用这个方法;这个类不需要关心这个字符串如何构建的,它只关心该方法是否返回内容。举例来说,让我们看看如何在另一个类调用此方法:
class ReportDownloadService { public function downloadPDF(BlogReport $report) { $name = $report->getName(); // 下载文件…… } }
尽管上面的代码正常运行,但我们设想一下,现在想给 UsersReport
类中增加下载用户报告的功能。显然,我们不能使用 ReportDownloadService
中的现有方法,因为我们已经强制规定方法只能传递 BlogReport
类。因此,我们必须修改把原有的下载方法名称改掉(避免重名),然后另外再添加一个类似的方法,如下所示:
class ReportDownloadService { public function downloadBlogReportPDF(BlogReport $report) { $name = $report->getName(); // 下载文件…… } public function downloadUsersReportPDF(UsersReport $report) { $name = $report->getName(); // 下载文件…… } }
假设上面的方法中的下载文件部分(注释掉的部分)使用了相同的代码,而且我们可以将这些相同的代码单独写成一个方法,但我们仍会有一些重复的代码(译者注:指的是每个方法中都会有 $name = $report->getName();
)以及有多个几乎相同的类的入口。这可能会给将来扩展代码或测试带来额外的工作量。
例如,假设我们创建了一个新的 AnalyticsReport
;我们现在需要向该类添加一个新的 downloadAnalyticsReportPDF()
方法。你可以清晰的看到这个文件将如何膨胀(译者注:指每增加一个类型,就要增加一个下载方法)。这就是一个使用接口的完美场景!
让我们从创建第一个接口开始:让我们将其命名为 DownloadableReport
,定义如下:
interface DownloadableReport { public function getName(): string; public function getHeaders(): array; public function getData(): array; }
我们现在可以更新 BlogReport
和 UsersReport
来实现 DownloadableReport
接口,如下例所示。但是请注意,作为演示用途,我故意把 UsersReport
中的代码写错了:
class BlogReport implements DownloadableReport { public function getName(): string { return 'Blog report'; } public function getHeaders(): array { return ['The headers go here']; } public function getData(): array { return ['The data for the report is here.']; } }
class UsersReport implements DownloadableReport { public function getName() { return ['Users Report']; } public function getData(): string { return 'The data for the report is here.'; } }
但当我们尝试运行代码的时候,我们将会收到错误,原因如下:
-
缺少
getHeaders()
方法. -
getName()
方法不包括接口的方法签名中定义的返回类型。 -
getData()
方法定义了一个返回类型,但它与接口的方法签名中定义的类型不同。
因此,为了修复 UsersReport
使其正确实现 DownloadableReport
接口,我们可以将其修改为:
class UsersReport implements DownloadableReport { public function getName(): string { return 'Users Report'; } public function getHeaders(): array { return []; } public function getData(): array { return ['The data for the report is here.']; } }
现在两个报告类都实现了相同的接口,我们可以这样更新我们的 ReportDownloadService
:
class ReportDownloadService { public function downloadReportPDF(DownloadableReport $report) { $name = $report->getName(); // 下载文件…… } }
我们现在可以把 UsersReport
或 BlogReport
对象传入 downloadReportPDF
方法中,而且不会出现任何错误。这是因为我们知道该对象实现了报告类的必要方法,并且将返回我们期望的数据类型。
通过向方法传递了一个接口,而不是一个具体的类,我们可以根据方法的实际作用(而不是方法的实现原理)来解耦 ReportDownloadService
类和这些报告类。
如果我们想创建一个新的 AnalyticsReport
,我们可以让它实现相同的接口。这样一来,我们不必添加任何新的方法,只需要将报告对象传递给同一个的 downloadReportPDF()
方法。如果你正在构建你自己的包或框架,接口可能对你特别有用。你只需要告诉使用者要实现哪个接口,然后他们就可以创建自己的类。例如,在 Laravel 中,我们可以通过实现 IlluminateContractsCacheStore
接口来创建自己的自定义缓存驱动类。
除了能改进代码之外,我喜欢使用接口的另一个原因是 —— 它们起到了“代码即文档”的作用。例如,如果我想弄清楚一个类能做什么,不能做什么,我倾向于先看接口,然后再看实现它的类。接口能够告诉我们所有可被调用的方法,而不需要我们过多地关心这些方法的底层实现方式是怎样的。
值得注意的是,Laravel
中的“契约(contract)”和“接口(interface)”这两个词语是可互换的。根据 Laravel 文档,“契约是一组由框架提供的核心服务的接口”。所以,记住:契约是一个接口,但接口不一定是契约。通常情况下,契约只是框架提供的一个接口。关于使用契约的