Laravel 单行为控制器设计的魅力
CRUD 和领域建模的比较
在开始之前,我们首先考虑一下编写明智的 CRUD 控制器的倾向。我确信很多人会坚持使用这种方法,因为这是 Laravel 中的标准做法,并且文档中的大多数示例都使用这种方法。此外,这可能是您在各种博客或应用程序代码中经常看到的内容。
但是当你停下来想一想,这是最好的写作方式吗?这是软件行业的常见做法吗?近年来,我花了很多时间在领域驱动设计等领域,并思考软件如何应用于您正在使用的领域(Domian)以及如何翻译它。当您开始思考模仿您所在领域中各处语言的术语和措辞时,您会发现您的代码会变得越来越清晰。 (最后这句话还是值得思考和改进的)
最后,我认为写软件的本质就是尽可能的实现领域流程,让你的代码具有可读性和可维护性。
合理的控制器不能很好地完成这两件事。首先,它的可读性较差,因为您倾向于编译数据而不是域。在这种情况下,您将失去上下文控制。您展示了数据是如何处理的,但没有解释到底发生了什么,或者用于处理它的过程。
其次,您没有针对维护进行优化。由于您是围绕数据结构构建的,因此您还可以将它们连接起来。事实上,您的领域模型在不断发展,您的数据结构也在不断发展。如果您的数据结构处理多个进程或多个域部分,那么将很难管理。
实际例子
既然理论比较枯燥,用代码更容易解释,所以我们看一个实际例子。
假设您创建一个允许用户管理事件的应用程序。您希望提供一种创建、更新和删除这些事件的方法。这是一个非常典型的例子,说明了您如何考虑 CRUD 术语的实现。那么,我们来看看这款智能控制器是如何改造的。
首先我们看一下路由:
Route::get('events', [EventController::class, 'index']); Route::get('events/create', [EventController::class, 'create']); Route::post('events', [EventController::class, 'store']); Route::get('event/{event}', [EventController::class, 'show']); Route::get('events/{event}/edit', [EventController::class, 'edit']); Route::put('events/{event}', [EventController::class, 'update']); Route::destroy('events/{event}', [EventController::class, 'destroy']);
现在对应的控制器:
<?php namespace App\Http\Controllers; use App\Models\Event; final class EventController { public function index() { // ... } public function create() { // ... } public function store() { // ... } public function show(Event $event) { // ... } public function edit(Event $event) { // ... } public function update(Event $event) { // ... } public function destroy(Event $event) { // ... } }
这个EventController处理所有CRUD请求,显示事件列表,显示定义的事件,创建事件,更新现有事件和删除事件。
让我们看一下index方法的细节:
public function index() { $events = Event::paginate(10); return view('', compact('events')); }
在这个方法中,我们获取一个事件,然后给它一个视图以将其显示在分页列表中。到目前为止,一切都很好。但现在您想要实现一种方法来使用不同的页面来查看过去和未来的事件。我们来看看如何在index方法中实现:
public function index(Request $request) { if ($request->boolean('past')) { $events = Event::past()->paginate(10); } elseif ($request->boolean('upcoming')) { $events = Event::upcoming()->paginate(10); } else { $events = Event::paginate(10); } return view('', compact('events')); }
嗯嗯!看起来非常拥挤。尽管我们已经使用 Eloquent 范围来隐藏查询逻辑,但我们仍然有不好的链式语句。让我们看看如何使用单个行为控制器来改变这一点。
每个行为控制器只做一件事,而且只做一件事。
首先,我们不使用查询参数来获取不同事件的列表,而是使用自定义路由来获取它们。
Route::get('events', ShowAllEventsController::class); Route::get('events/past', ShowPastEventsController::class); Route::get('events/upcoming', ShowUpcomingEventsController::class);
这条路线比上一条稍微长一点,但是这条路线比上一条更具表现力。您可以一眼看出哪个控制器处理某些逻辑。如果您比较 URL,您将看到阅读方面的一些改进:
# Before /events /events?past=true /events?upcoming=true # After /events /events/past /events/upcoming
现在看看其中一个控制器。只需看一下 ShowUpcomingEventsController 控制器即可:
<?php namespace App\Http\Controllers; use App\Models\Event; final class ShowUpcomingEventsController { public function __invoke() { $events = Event::upcoming()->paginate(10); return view('', compact('events')); } }
缺少该语句很糟糕,我们已经为它们创建了一种方法来读取我们从第一个 CRUD 控制器示例中获得的三个衬垫。但我们现在不再拥有所有其他 CRUD 操作,而是拥有用于特殊操作的特殊控制器。
简单、易于阅读且易于维护。
你可以问问自己,值得吗?毕竟前面的说法还不错吧?但我想说的是,你优化以后的改进,提高可维护性。下次您想要对这三个页面进行特定更改时,您将知道在哪里进行更改,而不必执行 if 语句。
当然上面的例子很简单,我们来看看更复杂的。考虑重构 create 和 save 方法:
public function create() { return view(''); } public function store(Request $request) { $data = $request->validate([ 'name' => 'required', 'start' => 'required', 'end' => 'required|after:start', ]) $event = Event::create($data); return redirect()->route('', $event); }
我们需要做的是将这两个方法移至自定义控制器,这样可以更好地解释这些方法的作用。此方法比将其放入名为 ScheduleNewEventController 的控制器中更好。然后我们更新此控制器的路线:
Route::get('events/schedule', [ScheduleNewEventController::class, 'showForm']); Route::post('events/schedule', [ScheduleNewEventController::class, 'schedule']);
我不会向您展示确切的控制器,因为它们有两种方法,如上面的示例,只需重命名 showForm 和时间表以更具表现力命名它们的功能。尽管这不是单个行为控制器,但方法是相同的:将应用程序中的自定义行为(方法)与相应的控制器分开。
好的,现在您已经看到了单一行为控制器的示例。您可能认为这会产生更多文件。但实际上,这不是问题。文件太多也没关系。拥有更小、更容易维护的文件比拥有更大、更难分析的文件要好。您可以打开单行为控制器的文件并快速扫描代码并立即知道它是什么。
我经常将它们收集到不同的目录中,这些目录负责领域的不同部分。这使得从文件结构的角度更容易看到控制器。
分体式控制器还可以让您轻松找到特定的控制器。想象一下您正在寻找一个可以安排事件的控制器。现在您只需按文件名搜索编辑器,而不是通用的 EventController。
另一个案例
我也被问到是否要对所有控制器执行此操作。不总是。当谈到控制器名称时,我倾向于严格和简洁,但像你一样,我会适应这种情况。
当然,有时候你还是想用常识性的控制器。例如当您构建 RESTful API 时。这是有道理的,因为您经常直接与数据本身交互,而不是经常与任何域或进程交互。 CMS(内容管理系统)或 Laravel Nova 等应用程序就是最好的例子。
但如果有必要,最好问问自己,你的解决方案是否更接近领域和流程。如果您需要执行基于域的操作(例如 GraphQL 或类似 RPC 的 API),这可能更合适。
结论
我希望这有点有洞察力,现在您可以更好地理解为什么我如此喜欢单一行为控制器。我相信,组合小类、使用普遍存在的语言以及清晰地命名事物将导致更易于维护的代码,即使对于控制器而言也是如此,而不仅仅是域对象。但就像我一开始说的那样,选择对你有帮助的部分,并知道什么对你有用,什么对你无用。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。