支持 Laravel.io 的持续发展 →

Laravel 内部机制 - 策略模式

12 May, 2024 8 min read

你好 👋

维基百科:在计算机编程中,策略模式(也称为策略模式)是一种行为型软件设计模式,它允许在运行时选择算法的行为。

首次,维基百科在IT环境下的定义对我来说有意义。不过,我们将在本文中讨论策略模式以及Laravel如何在内部使用它。在Laravel社区中,它通常被称为“管理器模式”。我还在一本书中遇到过被标记为“构建器”模式的情况,我认为这不正确,稍后我会解释原因。简单来说,策略模式允许您根据条件切换实现(或算法)。在我们深入研究之前,重要的是要理解这些模式并非神圣的经文;它们可以用各种方式实现 🤷。模式总是解决同一个问题,但可能会引入一些微调,这正是Laravel所做的事情。

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

在Laravel中,您可能已经调用过driver()方法(至少一次),使用Cache门面,邮件发送或日志记录。让我们以缓存为例。

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

这将使用数据库驱动程序来缓存值'bar'。现在的问题是我们不想强迫用户使用单一的驱动程序;他们可以选择不同的驱动,例如文件驱动程序、Redis驱动程序,或者任何其他驱动程序。因此,我们需要一种方法来根据用户设置的条件或影响进行驱动程序之间的切换。如果没有设置任何内容,我们始终可以回退到默认驱动程序。

Laravel通过实现策略模式(或管理器模式)解决这个问题,允许您设置要使用的驱动程序。例如,如果我们想使用文件驱动程序而不是数据库,我们可以简单地调用driver()方法

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

将使用文件驱动程序而不是数据库,因为我们更改了一个条件,该条件将在运行时选择行为(实现)。让我们看看是怎么回事。

揭秘魔法

当您在门面上调用电驱动程序方法时,它取决于您使用的门面代理到管理器。在缓存场景中,它被导向CacheManager。让我们检查其代码。

想知道我们是如何从门面到达管理器的?我强烈推荐阅读"Facades Under The Hood"

<?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()方法,而是扩展了基礎管理器,它已经包含了这个方法。现在,您所剩下的是将这个NotificationsManager包裹在一个Notification门面中。

本文不会介绍如何进行操作。这对你来说是一个好的练习。如果你卡住了?这里是如何做到的

假设我们创建了这个门面;您现在可以这样做

<?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

就这样,您创建了自己的管理器!

改变主意

在引言中,我提到了我不同意将管理器模式与构建器模式等同起来。也许作者认为与构建器模式比与策略模式更为相似,但对我来说,它们并不一致。构建器模式允许你以动态属性构建复杂对象。

维基百科:构建器模式是一种对象创建的软件设计模式,旨在解决递归构造器反模式。

构建器模式的基本实现可能如下所示

(new BurgerBuilder())
    ->addPatty()
    ->addLettuce()
    ->addTomato()
    ->prepare();

每个汉堡都可能不同;有的可能有奶酪,有的可能没有生菜(我恨生菜,它太糟糕了)。这个模式非常适合,否则我们就会得到这样一个臃肿的构造器:

public function __construct($cheese = true, $patty = true, $tomato = true, $lettuce = false)
{
}

类似地,在Laravel中发送电子邮件时,我们这样做:

Mail::to($request->user())
    ->cc($moreUsers)
    ->bcc($evenMoreUsers)
    ->when($this->condition(...))
    ->locale($request->user()->locale)
    ->send(new OrderShipped($order));

每个邮件对象都会根据情况进行变化;一个对象可能只使用 to() 方法。如果没有这个实现,我们就会得到这样一个构造器:

public function __construct($users, $cc = null, $bcc = null, $when = null, $locale = null)
{
}

看起来很相似,对吧?它们几乎一模一样。

好吧,一个用于汉堡,另一个用于邮件,但是嘿,你让我知道了 🍔

Laravel的例子被称为“挂起模式”,这实际上是一种构建器模式。这就是为什么我更喜欢不将管理器模式与后者相关联。

如果你想知道关于挂起对象的文章,请告诉我!

结论

模式解决问题。你不必无处不在都使用它们,也不必过度使用。然而,了解哪种模式适用于哪种情况,以及你在框架中使用的是哪种模式,是非常有价值的。这种理解可以简化你的工作流程。正如你所看到的,我们只用了几行代码就实现了我们的管理器,因为我们理解了Laravel的工作原理。不要期望实现或命名总是相同; 它们并非神圣文本。它们可以被调整以满足你的需求。问题可能相似,但并不总是相同的。

最后更新时间:2个月前。

driesvints, krishat2017, flavius-constantin 赞同了这篇文章

3
喜欢这篇文章? 让作者知道,并给他们点赞!
oussamamater (Oussama Mater) 我是一名软件工程师和CTF玩家。我使用Laravel和Vue.js将想法转化为应用程序 🚀

你可能还感兴趣的其他文章

如何使用 Larastan 将您的 Laravel 应用从0到9推进

在 Laravel 应用执行前,就可以通过 Larastan 查找其中的错误,它...

阅读文章

在无需特性( Traits )的情况下进行 API 响应标准化

我发现,大多数用于 API 响应的库通常使用特性( Traits )实现...

阅读文章

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

如何在 Laravel 项目中创建反馈模块,并在收到信息时通过 Discord 发送通知...

阅读文章

感谢以下这些 极具魅力的公司 支持我们

这里的您的标志?

Laravel.io

Laravel 解决方案、知识共享和社区建设的门户网站。

© 2024 Laravel.io - 所有权利保留。