使用接口编写更好的 PHP 代码

2025-06-08

使用接口编写更好的 PHP 代码

介绍

在编程中,确保代码的可读性、可维护性、可扩展性和易测试性至关重要。在代码中,使用接口是提升这些方面的方法之一。

目标读者

本文面向对 OOP(面向对象编程)概念有基本了解并在 PHP 中使用继承的开发人员。如果您知道如何在 PHP 代码中使用继承,那么希望本文对您有所帮助。

什么是接口?

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

接口可以是:

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

接口不能

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

接口用于定义类应该包含的公共方法。需要记住的是,接口只定义了方法签名,而不包含方法体(就像类中常见的方法一样)。这是因为接口仅用于定义对象之间的通信,而不是像类那样定义通信和行为。为了提供一些背景信息,此示例展示了一个定义了多个公共方法的示例接口:

interface DownloadableReport
{
    public function getName(): string;

    public function getHeaders(): array;

    public function getData(): array;
}
Enter fullscreen mode Exit fullscreen mode

根据php.net,接口有两个主要用途:

  1. 允许开发者创建不同类的对象,这些对象由于实现了相同的接口而可以互换使用。一个常见的例子是多个数据库访问服务、多个支付网关或不同的缓存策略。不同的实现可以互换,而无需对使用它们的代码进行任何更改。
  2. 允许函数或方法接受并操作符合接口的参数,而无需关心该对象的其他功能或实现方式。这些接口通常以 Iterable、Cacheable、Renderable 等命名,以描述行为的意义。

在 PHP 中使用接口

接口是面向对象编程 (OOP) 代码库中不可或缺的一部分。它们使我们能够解耦代码并提高可扩展性。为了举例说明,我们来看看下面的这个类:

class BlogReport
{
    public function getName(): string
    {
        return 'Blog report';
    }
}
Enter fullscreen mode Exit fullscreen mode

如你所见,我们定义了一个类,其中包含一个返回字符串的方法。通过这样做,我们定义了该方法的行为,以便我们能够看到它getName()是如何构建返回的字符串的。但是,假设我们在另一个类的代码中调用此方法。另一个类不会关心字符串是如何构建的,它只关心它是否被返回。例如,让我们看看如何在另一个类中调用此方法:

class ReportDownloadService
{
    public function downloadPDF(BlogReport $report)
    {
        $name = $report->getName();

        // Download the file here...
    }
}
Enter fullscreen mode Exit fullscreen mode

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

class ReportDownloadService
{
    public function downloadBlogReportPDF(BlogReport $report)
    {
        $name = $report->getName();

        // Download the file here...
    }

    public function downloadUsersReportPDF(UsersReport $report)
    {
        $name = $report->getName();

        // Download the file here...
    }
}
Enter fullscreen mode Exit fullscreen mode

虽然你实际上看不到,但我们假设上面类中的其余方法使用相同的代码来构建下载。我们可以将共享代码提升到方法中,但仍然可能存在一些共享代码。此外,我们将在类中设置多个运行几乎相同代码的入口点。这可能会在将来尝试扩展代码或添加测试时导致额外的工作。

例如,假设我们创建了一个新的AnalyticsReport;现在我们需要向downloadAnalyticsReportPDF()该类添加一个新方法。你很可能会看到这个文件会迅速增长。这时就非常适合使用接口了!

让我们先创建一个;我们将DownloadableReport像这样调用和定义它:

interface DownloadableReport
{
    public function getName(): string;

    public function getHeaders(): array;

    public function getData(): array;
}
Enter fullscreen mode Exit fullscreen mode

现在我们可以更新BlogReportUsersReport实现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.'];
    }
}
Enter fullscreen mode Exit fullscreen mode
class UsersReport implements DownloadableReport
{
    public function getName()
    {
        return ['Users Report'];
    }

    public function getData(): string
    {
        return 'The data for the report is here.';
    }
}
Enter fullscreen mode Exit fullscreen mode

如果我们尝试运行代码,我们会因为以下原因而收到错误:

  1. getHeaders()缺少方法
  2. getName()方法不包含接口方法签名中定义的返回类型。
  3. 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.'];
    }
}
Enter fullscreen mode Exit fullscreen mode

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

class ReportDownloadService
{
    public function downloadReportPDF(DownloadableReport $report)
    {
        $name = $report->getName();

        // Download the file here...
    }

}
Enter fullscreen mode Exit fullscreen mode

现在,我们可以将UsersReportBlogReport对象传递给downloadReportPDF()方法,而不会出现任何错误。这是因为我们现在知道报表类所需的必要方法已经存在,并且返回的数据类型符合我们的预期。

由于将接口而不是类传递给方法,这使我们能够ReportDownloadService根据方法的功能而不是方法的操作来松散地耦合和报告类。

如果我们想创建一个新的AnalyticsReport,可以让它实现相同的接口,这样我们就可以将报表对象传递给相同的downloadReportPDF()方法,而无需添加任何新方法。如果您正在构建自己的包或框架,并希望让开发人员能够创建自己的类,这将特别有用。您只需告诉他们要实现哪个接口,他们就可以创建自己的新类。例如,在Laravel中,您可以通过实现接口来创建自己的自定义缓存驱动程序类Illuminate\Contracts\Cache\Store

除了使用接口来改进实际代码之外,我喜欢接口的另一个原因是它们充当了代码文档的功能。例如,如果我想弄清楚一个类能做什么、不能做什么,我倾向于先查看接口,然后再查看使用它的类。这样可以告诉你所有被调用的方法,而我不需要太关心这些方法在底层是如何运行的。

对于我的 Laravel 开发者读者来说,值得注意的是,“契约”和“接口”这两个术语经常被互换使用。根据Laravel 文档,“Laravel 的契约是一组定义框架提供的核心服务的接口”。因此,务必记住,契约是接口,但接口不一定是契约。通常,契约只是框架提供的接口。有关使用契约的更多信息,我建议您阅读文档,因为我认为它很好地解释了契约的含义、使用方法以及使用时机。

结论

希望通过阅读本文,您可以简要了解什么是接口、如何在 PHP 中使用接口以及使用接口的好处。

对于我的 Laravel 开发者读者,我将在下周撰写一篇新的博客文章,向您展示如何在 Laravel 中通过接口使用桥接模式。如果您对此感兴趣,请随时在我的网站上订阅我的新闻通讯,以便在我发布时收到通知。

如果这篇文章有助于你理解界面,欢迎在评论区留言。继续创造精彩作品吧!🚀

非常感谢Aditya KadamJae TooleHannah Tinkler校对这篇文章并帮助我改进它!

更新:关于如何在 Laravel 中使用桥接模式的后续文章现已发布。点击此处查看!

鏂囩珷鏉ユ簮锛�https://dev.to/ashallendesign/using-interfaces-to-write-better-php-code-391f
PREV
2020 年 React 女性回顾!
NEXT
我为 GitHub 问题制作了一台收据打印机