面向 фаса的探索
发布日期 作者 史蒂夫·麦克杜格尔
Facades,人们似乎要么喜欢要么讨厌。无论如何,它们都是今天 Laravel 的一个自然组成部分。然而,Laravel Facades 并不严格意义上的Facades;是吗?相反,它们是用于从容器中解析类的静态访问器。
当我第一次开始使用 Laravel 时,我讨厌它,在使用 Larvel 三年后,我终于开始接受它。我是你今天典型的 Laravel 讨厌者。当我知道拥抱 Laravel 的本来面目,而不是试图用我的思维方式来对抗框架时,我才学会了爱它。有些人可能会说我现在是它最大的拥护者之一。
有一段时间,我讨厌的一件事就是 Facades。我在抱怨静态方法调用,等等。但我不知道它们在 Laravel 中是如何工作的。我只是加入了其他开发者的噪音,重复他们说过的话,却不知道自己在说什么。
快进到今天,我理解了 Facades 的工作原理——你知道吗?我确实改变了我的看法。我想写这个教程,不是为了让你都同意我的观点,虽然你应该同意,而是为了让你也理解 facades 的工作原理以及它们的优势所在。
这不是严格意义上的教程,因为我会带你浏览我已经写过的现有代码,而我通常会在写教程的时候写代码,这样我就可以解释自然重构点。我将带你浏览的代码是 Get Send Stack Laravel 包,你可以在 GitHub 上找到它.
在构建这个包时,我做了我通常做的事情,开始使用 HTTP Facade 构建一个 API 集成——使用接口/契约来利用 DI 容器在需要时注入实例。让我带你浏览这些代码阶段。我们先从不使用 DI 容器开始。
class AddSubscriberController{ public function __invoke(AddSubscriberRequest $request) { $client = new Client( url: strval(config('services.sendstack.url')), token: strval(config('services.sendstack.token')), ); try { $subscriber = $client->subscribers()->create( request: new SubscriberRequest( email: $request->get('email'), firstName: $request->get('first_name'), lastName: $request->get('last_name'), optIn: $request->get('opt_in'), ), ); } catch (Throwable $exception) { throw new FailedToSubscribeException( message: $exception->getMessage(), previous: $exception, ); } // return redirect or response. }}
所以这有点冗长,但很清楚地看到了你在做什么。你创建客户端,通过客户端发送请求,并捕获可能的异常。最后,根据是 API 还是 Web 控制器,返回重定向或响应。这段代码并不差。你可以测试它。你可以在控制器中轻松地确保行为。
但是,如果该包改变了与它的集成方式,你必须遍历你的代码库,并根据需要进行所有更改,因为你在任何地方都使用新的客户端来处理 API。这是一个考虑重构的最佳时机,因为你通过更聪明地工作来节省未来的工作。直接使用 DI 容器,让我们来看看上面代码的重构版本。
class AddSubscriberController{ public function __construct( private readonly ClientContract $client, ) {} public function __invoke(AddSubscriberRequest $request) { try { $subscriber = $this->client->subscribers()->create( request: new SubscriberRequest( email: $request->get('email'), firstName: $request->get('first_name'), lastName: $request->get('last_name'), optIn: $request->get('opt_in'), ), ); } catch (Throwable $exception) { throw new FailedToSubscribeException( message: $exception->getMessage(), previous: $exception, ); } // return redirect or response. }}
现在更简洁、更易于管理,我们从 DI 容器中注入契约/接口,它将为我们解析客户端——因为包服务提供者已经详细说明了如何构建客户端。这种方法没有什么问题;它是我在代码中大量使用的模式。我可以替换实现以获得不同的结果,并且仍然使用相同的包 API,因为我使用的是接口/契约。但同样,当我使用容器时——我是否在对抗框架?我们中许多人喜欢 Laravel 的原因之一是它的开发体验,我们应该感谢 Eloquent。我们不必费心与容器进行杂耍来创建一个新模型或类似的东西。我们非常习惯在需要的时候静态调用我们想要的东西。所以让我们看看上面使用我在包中创建的 Facade 的示例。
class AddSubscriberController{ public function __invoke(AddSubscriberRequest $request) { try { $subscriber = SendStack::subscribers()->create( request: new SubscriberRequest( email: $request->get('email'), firstName: $request->get('first_name'), lastName: $request->get('last_name'), optIn: $request->get('opt_in'), ), ); } catch (Throwable $exception) { throw new FailedToSubscribeException( message: $exception->getMessage(), previous: $exception, ); } // return redirect or response. }}
不再需要担心容器——我们又回到了我们所缺少的熟悉的 Laravel 感觉。这里的优点是开发体验简单直观,实现看起来很干净,并且我们获得了相同的结果。缺点是什么?当然,有一些缺点。唯一的缺点是,你不能切换实现,因为 Facade 对它的实现是静态的。但根据我的经验,当你谈论外部服务时,从提供商 A 迁移到提供商 B 比创建和绑定一个新的实现到容器要复杂得多。那些总是敲打这面鼓的人用狭隘的意识形态范围来看待这个问题。实际上,更改提供商是一项巨大的工作,不仅仅是从代码的角度来看——所以总有足够的时间专注于在需要的地方实现不同的东西。有时,新提供商有一些老提供商没有的东西。也许你必须在你的请求中发送额外的數據等等。
我的观点是,虽然 SOLID 原则是很棒的,你应该参考它们来获得建议——但它们通常是不切实际的梦想,在实践中行不通,或者你将花费太多时间编写功能,以至于范围在你完成之前就发生了变化。在每个转折点对抗框架并不能帮助你构建好的产品。你通过接受不完美并承认可能需要改变来创建好的产品。
这与 Facades 有什么关系?正如你从代码示例中看到的,Facades 在许多方面使事情变得更容易。这两种方式都没有错,也没有哪种方式是对的。Facade 将允许更友好的实现,但会迫使你走上一条特定的道路。使用容器将允许你未来有更大的灵活性,但它不是万能药,也存在自身的风险。当你需要的时候简单地新建实例很容易,但当有更好的方法来实现相同的结果时,这样做就很懒惰了。
Facade 到底长什么样?这是包中的确切代码。
declare(strict_types=1); namespace SendStack\Laravel\Facades; use Illuminate\Support\Facades\Facade;use SendStack\Laravel\Contracts\ClientContract;use SendStack\Laravel\Http\Resources\SubscribersResource;use SendStack\Laravel\Http\Resources\TagResource; /** * @method static SubscribersResource subscribers() * @method static TagResource tags() * @method static bool isActiveSubscriber(string $email) * * @see ClientContract */class SendStack extends Facade{ protected static function getFacadeAccessor() { return ClientContract::class; }}
它有一个受保护的静态方法,用于获取它需要构造和构建的类,并且我们扩展的类将把所有静态调用转发到这个类,一旦从容器中解析出来。人们把 Facades 说成是脏话,但实际上,它与创建容器别名没什么区别,只是语法不同。在我的例子中,我在实现/接口上的方法中添加了文档块,以便更好地进行 IDE 完成——但这只是一个我喜欢采取的额外步骤。
这个故事的寓意是,Facades 并不邪恶,实际上可能非常有用——所以忽略那些讨厌它的人,像我一样拥抱它。你会为此感到高兴,并且会更加高效。