编程

Laravel 底层 - 策略模式

105 2024-05-25 00:19:00

策略模式是一种行为设计模式,使得算法行为可以在运行时被选择。

我们将在本文中讨论策略模式,以及 Laravel 如何在幕后使用它。在 Laravel 社区中,它通常被称为 Manager 模式。我在书中也遇到过它被标记为 “Builder” 模式的情况,我不同意这一点,稍后我将解释原因。简单地说,策略模式允许你根据条件切换实现(或算法)。在我们深入研究之前,需要理解这些模式不是神圣的文本;它们可以通过各种方式实现🤷 。模式总是会解决同样的问题,但可能会引入一些调整,这正是 Laravel 所做的。

我们要解决什么问题以及如何解决?

在Laravel 中,当使用 Cache facade、使用 Mail 或日志时,你可能已经调用过 driver() 方法(至少一次)。让我们以缓存为例。

Cache::put(key: 'foo', value: 'bar');

这将会使用数据库驱动缓存值 ‘bar’ 。现在的问题是,我们不想强制用户使用单一的驱动,他们可以选择不同的驱动,比如文件驱动、Redis 驱动或其他驱动。因此,我们需要有一种方式,可以根据用户设置的条件切换这些实现。如果没有进行任何设置,则使用默认驱动。

Laravel 通过实现策略模式(或称为 Manager 模式)来解决这个问题。比如,如果我想实现文件驱动,而非数据库驱动,我们只需调用 driver() 方法:

Cache::driver('file')->put('foo', 'bar');

使用文件驱动而不是数据库,因为我们更改了条件,使之在运行时选择行为(实现)。让我们看看怎么做。

揭开魔法的神秘面纱

在 facade 上调用 driver 方法时,它会被代理到一个管理器,这取决于你使用的 facade。在缓存场景中,它被定向到 CacheManager。让我们查看一下它的代码。

<?php

namespace Illuminate\Cache;

use Illuminate\Contracts\Cache\Factory as FactoryContract;

/**
 * @mixin \Illuminate\Cache\Repository
 * @mixin \Illuminate\Contracts\Cache\LockProvider
 */
class CacheManager implements FactoryContract
{
    // omitted for brevity

    /**
     * Get a cache driver instance.
     *
     * @param  string|null  $driver
     * @return \Illuminate\Contracts\Cache\Repository
     */
    public function driver($driver = null)
    {
        return $this->store($driver);
    }

    // omitted for brevity
}

此处,你可以看到 driver() 方法,其接收一个驱动。该方法返回 store() 产生的结果。

<?php

namespace Illuminate\Cache;

use Illuminate\Contracts\Cache\Factory as FactoryContract;

/**
 * @mixin \Illuminate\Cache\Repository
 * @mixin \Illuminate\Contracts\Cache\LockProvider
 */
class CacheManager implements FactoryContract
{
    // omitted for brevity

    /**
     * Get a cache store instance by name, wrapped in a repository.
     *
     * @param  string|null  $name
     * @return \Illuminate\Contracts\Cache\Repository
     */
    public function store($name = null)
    {
        // This is the condition we modified by passing a driver.
        $name = $name ?: $this->getDefaultDriver();

        return $this->stores[$name] ??= $this->resolve($name);
    }

    // omitted for brevity
}

如果用户没有设置 $name(驱动),他默认使用配置项 cache.default 中指定的默认驱动。紧接着,它尝试解析该驱动。现在,我们来检查 resolve() 方法。

<?php

namespace Illuminate\Cache;

use Illuminate\Contracts\Cache\Factory as FactoryContract;

/**
 * @mixin \Illuminate\Cache\Repository
 * @mixin \Illuminate\Contracts\Cache\LockProvider
 */
class CacheManager implements FactoryContract
{
    // omitted for brevity

    /**
     * Resolve the given store.
     *
     * @param  string  $name
     * @return \Illuminate\Contracts\Cache\Repository
     *
     * @throws \InvalidArgumentException
     */
    public function resolve($name)
    {
        $config = $this->getConfig($name);

        if (is_null($config)) {
            throw new InvalidArgumentException("Cache store [{$name}] is not defined.");
        }

        $config = Arr::add($config, 'store', $name);

        if (isset($this->customCreators[$config['driver']])) {
            return $this->callCustomCreator($config);
        }

        $driverMethod = 'create' . ucfirst($config['driver']) . 'Driver';

        if (method_exists($this, $driverMethod)) {
            return $this->{$driverMethod}($config);
        }

        throw new InvalidArgumentException("Driver [{$config['driver']}] is not supported.");
    }

    // omitted for brevity
}

请注意,我们首先获取该驱动程序的配置,以便构建它。然后,我们检查开发人员是否扩展了缓存驱动。最后,我们按照 create[Name]driver 规范创建一个方法名称。本例中,它是 createFileDriver。之后,我们调用这个方法(它确实存在),并返回文件驱动的缓存实现。通过这种方式,在该实现上调用 put()get() 方法。
这意味着,如果我们调用 Cache::driver('redis'),我们将调用一个名为 createRedisDriver 的方法,该方法反过来将返回 redis 驱动的实现,以此类推。

如何使用

这就是深挖源码的美妙之处。如果想在不同的实现之间交换,现在可以在应用中使用它。有趣的是,已经有一个基础管理器可以使用了!
想象一下,我们正在构建一个通知系统。它有多种通道:短信、电子邮件、Slack 和 Discord。我们的代码如下所示:

<?php

namespace App\Managers;

use App\Notification\DiscordNotification;
use App\Notification\EmailNotification;
use App\Notification\SlackNotification;
use App\Notification\SmsNotification;
use Illuminate\Support\Manager;

class NotificationsManager extends Manager
{
    public function createSmsDriver() // create[Name]Driver
    {
        return new SmsNotification();
    }

    public function createEmailDriver() // create[Name]Driver
    {
        return new EmailNotification();
    }

    public function createSlackDriver() // create[Name]Driver
    {
        return new SlackNotification();
    }

    public function createDiscordDriver() // create[Name]Driver
    {
        return new DiscordNotification();
    }

    public function getDefaultDriver()
    {
        return 'slack'; // will turn into createSlackDriver
    }
}

你会发现,我们没有自己定义 driver() 方法,相反,我们扩展了已有的基础管理器。现在你所需要的是在 Notification 门面中包装 NotificationsManager

假定我们已经创建了门面,你可以这样

<?php

use App\Facades\Notification;

Notification::send(); // will use the default driver, slack
Notification::driver('discord') // will use the discord driver
Notification::driver('email') // will use the email driver
Notification::driver('sms') // will use the sms driver

小结

模式可以解决问题。你不必每处都使用,也不要过度使用。然而,了解哪种模式适合哪种情况以及框架中使用了什么是很有价值的。这种理解可以简化你的工作流程。如你所见,我们仅用几行代码就实现了 manager,因为我们了解 Laravel 的工作原理。不要期望实现或命名总是相同的;它们不是神圣的文本。可以根据需要进行调整。问题可能相似,但并不总是相同的。