面向接口编程 - Laravel 实践

面向接口编程是编码中的一种设计思想,这种方式基于接口而不是固定的类来构建应用程序。

如果您是一名程序员,那么您可能听说过则这样的说法,例如:面向接口编程、使用抽象类代替固定类等等。

这些都是说的同一件事,编写应用程序代码时,使其依赖抽象接口而不是具体的类。

为什么?

这是我第一次听到这句话时的确切反应。为什么要使用接口而不是类?即使创建了接口,我也需要创建一个实现该接口的类。这不是浪费时间吗?

当然不是!!

这个世界上唯一不变的就是变化本身, 也就是说, 变化是永恒的。

就编程而言, 这同样没有例外。业务需求随着时间变化, 我们的代码也要随之变化。

所以代码必需保持灵活。

面向接口编程可以使代码松散耦合且灵活。

怎么做?

观察下面的代码。

class Logger {

    public function log($content) 
    {
        //日志保存到文件中.
        echo "Log to file";
    }
}

这是一个将日志记录到文件的简单类。 我们可以在控制器中调用它。

class LogController extends Controller
{
    public function log()
    {
        $logger = new Logger;
        $logger->log('Log this');
    }
}

但如果需要将日志记录到多个地方(如数据库, 文件, 云端等)时, 我们又该怎么办呢。

然后我们可以更改 LogController 和 Logger 类以适应这些更改。

class Logger {

    public function logToDb($content) 
    {
        //将日志记录到 db.
    }

    public function logToFile($content) 
    {
        //将日志保存到 file.
    }

    public function logToCloud($content) 
    {
        //将日志存储到 cloud.
    }

}
class LogController extends Controller
{
    public function log()
    {
        $logger = new Logger;

        $target = config('log.target');

        if ($target == 'db') {
            $logger->logToDb($content);
        } elseif ($target == 'file') {
            $logger->logToFile($content);
        } else {
            $logger->logToCloud($content);
        }
    }
}

现在我们可以记录不同的目标了。但是,如果我们想将其他目标(例如日志)添加到 redis 服务器,该怎么办?最后,我们将同时修改 Logger 类和 LogController 类。

如您所见,这很快就摆脱了我们的控制,并且代码变得混乱。Logger 类很快成为一个整体。这是一场噩梦。

因此,我们需要拆分事物。遵循 SOLID 原则,我们可以将职责移至相应的类。

class DBLogger
{
    public function log()
    {
        //将日志记录到 db
    }
}

class FileLogger
{
    public function log()
    {
        //将日志保存到 file
    }
}

class CloudLogger
{
    public function log()
    {
        //将日志存储到 cloud
    }
}

并且控制器更改为:

class LogController extends Controller
{
    public function log()
    {
        $target = config('log.target');

        if ($target == 'db') {
            (new DBLogger)->log($content);
        } elseif ($target == 'file') {
            (new FileLogger)->log($content);
        } else {
            (new CloudLogger)->log($content);
        }
    }
}

这样就好多了。现在如果要添加其他日志记录目标,我们可以创建一个新类并将其添加到 Controller 中的 if-else。

但是,我们的控制器仍然负责选择记录器。对于控制器,不需要知道不同的记录器并在它们之间进行选择。它只需要一个带有 log() 方法的记录器类来记录内容。

使用接口

这种情况就适合使用接口。那么什么是接口?

接口是对对象可以执行的操作的描述。

以我们的示例为例,控制器仅需要带有 log() 方法的记录器类。因此,我们的接口必须描述它必须具有log() 方法。

interface Logger
{
    public function log($content);
}

如您所见,它仅包含函数声明,而不包含其实现,这就是为什么将其称为抽象的原因。

实现接口时,实现接口的类必须提供接口中定义的抽象方法的实现细节。

在我们的示例中,任何实现 Logger 接口的类都必须提供抽象方法 log() 的实现细节。

然后,我们可以在控制器中注入此接口。

class LogController extends Controller
{
    public function log(Logger $logger)
    {
        $logger->log($content);
    }
}

现在,控制器不再关心传递给它的记录器类型。它需要知道的是它必须实现Logger接口。

因此,我们需要修改 Logger 类以实现此接口。

class DBLogger implements Logger
{
    public function log()
    {
        //将日志记录到 db
    }
}

class FileLogger implements Logger
{
    public function log()
    {
        //将日志存储到 file
    }
}

class CloudLogger implements Logger
{
    public function log()
    {
        //将日志保存到 cloud
    }
}

现在,我们可以添加更多记录器,而无需触及现有代码。我们要做的就是创建一个实现Logger接口的新类。

class RedisLogger implements Logger
{
    public function log()
    {
        //将日志存储到 redis
    }
}

我们的代码现在看起来就变得灵活,低耦合了,我们可以随时改变实现方式而不用去改动之前的代码。

依赖注入

当我们使用的是 Laravel 框架, 我们可以使用服务容器去自动注册接口的实现。

因为 Laravel 提供开箱即用的方法注入,所以我们只需要把接口和实现绑定起来。

首先我们需要创建一个 logger 的配置文件。 就像这样

<?php

return [
    'default' => env('LOG_TARGET', 'file'),

    'file' => [
        'class' => App\Log\FileLogger::class,
    ],

    'db' => [
        'class' => App\Log\DBLogger::class,
    ],

    'redis' => [
        'class' => App\Log\RedisLogger::class,
    ]
];

然后在 app/Providers 路径下 AppServiceProvider.php 的文件添加如下代码

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        $default = config('log.default');
        $logger = config("log.{$default}.class");

        $this->app->bind(
            App\Contracts\Logger::class, // the logger interface
            $logger
        );
    }
}

这样做的效果是,从 logger.php 配置文件读取默认的 logger ,然后绑定到 Logger interface 。这样当我们使用 Logger interface ,容器将会帮我们解析并返回默认的 Logger 实例。

因为默认的 logger 是使用 env() 助手指定的,所以我们可以在不同的环境使用不同的 logger ,例如本地环境使用 file ,生产环境使用 db 。

总结

使用接口可以让我们写出低耦合的代码,提供一个抽象层。它允许我们随时改变实现方式。所以,尽可能将你的应用中可变的部分用面向接口的方式实现。

感谢阅读

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://dev.to/jinoantony/code-to-interf...

译文地址:https://learnku.com/laravel/t/44801

本帖已被设为精华帖!
本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 3

我这壶水就欠这点火候,我摊牌了,我沸腾了

3年前 评论

我这壶水就欠这点火候,我摊牌了,我沸腾了

3年前 评论

有没有什么办法能自动绑定实现类

2年前 评论
Marrigan

@tiankong 依赖注入

2年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!