支持Laravel.io的持续开发 →

PHP中的接口与抽象类

21 Aug, 2021 8 min read

简介

我最近发表了一篇关于如何使用接口编写更好的PHP代码的博客文章。它涵盖了接口的基本概念、它们的作用以及如何使用它们来提高PHP代码的可扩展性和可维护性。

在文章中,我收到一些开发者的主要评论,他们想知道“在什么情况下我应该使用接口而不是抽象类?”。因此,我想写这篇文章来解释PHP中抽象类与接口之间的区别,并简要概述你应该在何时使用它们。

什么是接口?

简单的来说,接口应该描述实现它们的类将如何构建,它们就像一个蓝图,描述了它们应该包含的公共方法和常量。

接口可以

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

接口不能

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

接口用于定义类应该包含的公共方法。重要的是要记住,接口总是意味着将由一个类实现,因此这里你只定义方法的签名,例如

interface HomeInterface
{
    const MATERIAL = 'Brick';

    public function openDoor(): void;

    public function getRooms(): array;

    public function hasGarden(): bool;
}

而不是像这样

interface HomeInterface
{
    public string $material = 'Brick';

    public function openDoor(): void
    {
        // Open the door here...
    }
    
    public function getRooms(): array
    {
        // Get the room data here...
    }
 
    public function hasGarden(): bool
    {
        // Determine if the home has a garden...
    }
}

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

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

使用我们上面的接口,并继续使用房屋类比,我们可以创建实现 HomeInterface 的不同类,如 HouseFlatCaravan。通过使用接口,我们可以确保我们的类包含必要的 3 个方法,并且所有方法都使用正确的方法签名。例如,我们可以有一个如下所示的 House

class House implements HomeInterface
{
    public function openDoor(): void
    {
        // Open the door here...
    }
    
    public function getRooms(): array
    {
        // Get the room data here...
    }
 
    public function hasGarden(): bool
    {
        // Determine if the home has a garden...
    }
}

什么是抽象类?

抽象类与接口非常相似;它们不是为了独立实例化而设计的,而是提供一个基线实现,让您可以扩展。

以我们上面关于房屋的例子,如果接口是你的蓝图,那么抽象类就是你的展示模型。它可以工作,并且是家庭的一个非常好的例子,但你仍然需要装修和装饰它,使其成为你自己的。

抽象类 可以 用作

  • 使用“抽象”方法(类似于接口)为类定义方法签名。
  • 定义方法。
  • 用于定义类的常量。
  • 用于定义类的属性。
  • 被子类扩展。

抽象类 不能

  • 单独实例化。

为了了解这意味着什么,让我们看看一个抽象类的例子

abstract class House
{
    const MATERIAL = 'Brick';
  
    abstract public function openDoor(): void;
    
    public function getRooms(): array
    {
        return [
            'Bedroom',
            'Bathroom',
            'Living Room',
            'Kitchen',
        ];  
    }
 
    public function hasGarden(): bool
    {
        return true;
    }
}

我们的 House 类是抽象的,这意味着我们无法直接实例化这些。要使用它,我们需要从它继承。例如,让我们创建一个扩展我们的 House 抽象类的 MyHouse

class MyHouse extends House
{  
    public function openDoor(): void
    {
        // Open the door...
    }
    
    public function getRooms(): array
    {
        return [
            'Bedroom One',
            'Bedroom Two',
            'Bathroom',
            'Living Room',
            'Kitchen',
        ];  
    }
}
// This will not work:
$house = new House();

// This will work:
$house = new MyHouse();

你可能会注意到,在 House 类中,我们声明了一个名为 openDoor() 的抽象公有方法。这基本上允许我们定义一个方法签名,子类应该以类似的方式包含,就像我们使用接口一样。这非常有用,如果你想要在子类之间共享一些功能,同时又强制它们包含自己的一些方法的实现。

在这个特定的例子中,子类可以像平常一样覆盖 getRooms()hasGarden() 方法,但不需要包括它们。为了展示这一点,我们覆盖了 getRooms() 方法,以显示我们如何在子类中更改其行为。

如何决定使用哪个

实际上将取决于你的目标。继续我们的房屋类比,如果你正在创建蓝图,以后可以用它来设计不同类型的房屋,那么你需要一个接口。

如果你已经建造了一座房子,现在你需要定制化地制作复制品,那么你需要一个抽象类。

让我给你举一些例子

何时使用接口

为了帮助我们理解何时使用接口,让我们看一个例子。假设我们有一个包含 buildHome() 方法的 ConstructionCompany 类,该方法的代码如下

class ConstructionCompany
{
    public function buildHome($home)
    { 
        // Build the home here...
	  
        return $home;
    }
}

现在,假设我们有 3 个不同的类,我们希望能够构建并将其传递给 buildHome() 方法

  1. class MyHouse implements HomeInterface extends House
  2. class MyCaravan implements HomeInterface
  3. class MyFlat implements HomeInterface

我们可以看到,MyHouse类继承了House抽象类;从概念上讲,这是因为房屋就是房屋,所以这是有道理的。然而,对于MyCaravanMyFlat类来说,从抽象类中扩展就没有意义,因为它们都不是房屋。

因此,由于我们的建筑公司能够建造房屋、房车和平房,这就排除了我们在buildHome()方法中对$home参数类型提示为House实例的情况。

然而,这正是我们对该方法进行类型提示以便只能传递实现HomeInterface的类的完美场所。例如,我们可以将方法更新为:

class ConstructionCompany
{
    public function buildHome(HomeInterface $home)
    { 
        // Build the home here...
	  
        return $home;
    }
}

通过这样做,我们可以确保无论我们传递的是房屋、房车还是平房,我们的ConstructionCompany类都会获得我们所需的信息,因为传入的home对象将始终包含我们所需的方法。

你可能也想过自己:“为什么我们不直接创建一个Home抽象类而不是一个接口?”然而,重要的是要记住PHP只支持单一继承,并且一个类不能从多个父类继承。所以,如果你想在未来扩展你的某个类,这会变得相当困难。

何时使用抽象类

让我们以与上述示例类似的情况为例。让我们想象我们有一个与我们的ConstructionCompany相似的HouseConstructionCompany。但在这个例子中,我们将假设HouseConstructionCompany只建造房屋,不做其他。

因为我们知道我们只需要能够建造房屋,所以我们可以将方法类型提示只接受扩展House抽象类的类。这非常有用,因为我们总是可以确信我们没有把任何其他类型的房屋传递给建设公司不建造的方法。例如:

class HouseConstructionCompany
{
    public function buildHouse(House $house)
    { 
        // Build the house here...
	  
        return $house;
    }
}

结论

希望这篇帖子能让您了解PHP中接口和抽象类的区别。它还应该为您提供了一个使用它们的场景简短的概述。

如果这篇帖子对您有所帮助,我非常乐意听到您的意见。同样,如果您有任何改进这篇帖子的反馈,我也非常乐意听到。

如果您对我的每篇新文章更新感兴趣,请随时注册我的通讯

对于任何寻找关于接口进一步阅读的读者,您可以在这里阅读如何使用接口在Laravel中实现策略模式的使用方法这里。

继续打造令人惊叹的作品! 🚀

上次更新1年前。

joedixon, driesvints, manishpeter, tim-frensch, ash-jc-allen, kenny201, ajmeireles, mikenk2010, ovillafuerte94, rsmsp 等人喜欢了这篇文章

11
喜欢这篇文章? 告诉作者并对他们表示支持!
ash-jc-allen (Ash Allen) 我是一个来自英国的普雷斯顿的自由职业Laravel Web开发人员。我维护Ash Allen Design博客,并在很多酷炫和令人激动的项目上工作 🚀

你可能喜欢的其他文章

2024年3月11日

使用Larastan将你的Laravel应用从0到9进行优化

在你的Laravel应用执行之前发现错误是可能的,归功于Larastan...

阅读文章
2024年7月19日

无需特性进行API响应标准化

我发现大多数用于API响应的库大多使用特性实现...

阅读文章
2024年7月17日

在Laravel项目中通过Discord通知收集反馈

如何在Laravel项目中创建一个反馈模块并在收到消息时...

阅读文章

我们感谢这些 令人惊叹的公司 对我们的支持

在这里放置您的标志?

Laravel.io

Laravel问题解决、知识共享和社区建设的门户。

© 2024 Laravel.io - 版权所有。