昨天,Jeffrey Way 发布了一条推文,他问大家更愿意将其控制器命名为单数还是复数。 我回答我两种方案都不选,我使用单动作控制器。随后发生的是,有的人同意,有的不同意,有的甚至做出了最奇怪的事情。
由于十分强烈的反映,我想写一篇文章来解释为什么我爱单行为控制器、还有我为什么觉得它们很美妙。
首先在开始文章之前,我想要说这个东西并不是只有单一的真相。与往常一样,我想指出的是,一切都归结于你的个人喜好。我只能教、建议和指出一些事情,由你来决定是否同意、不同意、接受、学习和 / 或调整。或者都不是。从这篇博客中获得你想要的,随心所欲地做让自己感到舒适的事情吧。
对比 CRUD 和 Domain Modelling
开始前,我们先来想想我们倾向于写 resourceful 的 CRUD 控制器。我相信很多人会坚持使用这种做法,因为这是 Laravel 中的一个标准做法,文档中的大多数示例也是使用这种方法。另外,这或许也是你在各类博客或 app 代码中经常看到的。
但是,如果你停下来思考一下,这是编写它们的最佳方法吗?是软件行业的一般性做法吗?最近几年,我在 “领域驱动设计”(Domain Driven Design) 等领域投入了大量时间,并且思考软件如何应用于你工作的领域(Domian)以及它转化的过程。当您开始考虑模仿您领域中无处不在的语言的术语和措辞时,您会发现您的代码将变得更加清晰明了,更加戳到点子上。(最后这一句话仍值得斟酌、改进)
最后,我相信编写软件的本质是尽可能地应用 domain processes 来让你的代码更加易读和更加可维护。
Resourceful 控制器在这两个方面做得并不好。首先,它们不易读,因为您倾向于根据数据来构建它们,而不是根据领域来构建它们。这样的话,你就会丢失上下文对照。你表现了数据的处理方式,但却没有说明到底发生什么了,也没有说明你使用哪个过程进行处理。
第二,你没有针对可维护性进行优化。由于你是根据数据结构来构建的,因此你也会跟着耦合进去。实际上,您的领域模型在不断发展,数据结构也在不断发展。如果你的数据结构处理着多个过程或领域的多个部分,那你将很难进行调整。
一个实际的例子
因为理论很无聊,上代码更加容易解释,所以我们来看一个实际的例子。
假设您正在构建一个应用,它允许用户去组织事件。您想提供一种创建,更新和删除这些事件的方法。这是一种非常典型的例子,你会用 CRUD 的方式来考虑实现它。那么,让我们看看就这样一个 resourceful 控制器是如何被转换的。
首先我们来看看路由:
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 AppHttpControllers; use AppModelsEvent; 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('events.index', 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('events.index', compact('events')); }
呃啊!看起来好乱啊。尽管我们已经用 Eloquent scopes 来隐藏查询逻辑,但是还是有很丑的链式语句。我们来看看如何用单行为控制器来代替它。
每个单行为控制器只执行一件事情,仅仅一件事情。
首先,我们不使用查询参数去获得不同的事件列表,而是使用专用路由去实现它。
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 AppHttpControllers; use AppModelsEvent; final class ShowUpcomingEventsController { public function __invoke() { $events = Event::upcoming()->paginate(10); return view('events.index', compact('events')); } }
丑陋的 if 语句没了, and has made way for the same readable three liner we had from our first CRUD controller example. But instead of having all of the other CRUD operations we now have a dedicated controller for a dedicated action.
简单,易读,便于维护。
你可能会问自己,这样做值么,毕竟之前的 if 语句也没那么坏吧?但是我想向你展示的是你正在为未来的改进做优化,并改进维护性。下次你想要对这三个页面做任何指定改变的时候,你会知道在哪里改,并且不需要艰难地更新一个 if 语句。
当然,上面的例子很简单,我们来看一个更复杂一点的。我们试试重构 create 和 store 方法:
public function create() { return view('events.create'); } public function store(Request $request) { $data = $request->validate([ 'name' => 'required', 'start' => 'required', 'end' => 'required|after:start', ]) $event = Event::create($data); return redirect()->route('event.show', $event); }
我们要做的就是把这两个方法移到专用的控制器,这样更好地解释了这些方法做了啥。这些方法更好地服务于你,比起把它们放在一个叫做 ScheduleNewEventController 的控制器中。我们接着更新这个控制器的路由:
Route::get('events/schedule', [ScheduleNewEventController::class, 'showForm']); Route::post('events/schedule', [ScheduleNewEventController::class, 'schedule']);
我不会向你展示一个确切的控制器,因为它们有和上面的例子一样,有两个方法,只不过把 showForm 和 schedule 重新命名为更能表达它们干了啥的名字。即使这个不是单行为控制器,但是方法论是一样的:把你应用中的专用行为(方法)和它对应的控制器拆分到一起。
好了,现在你已经看了单行为控制器的例子了。你可能会想,这会导致越来越多的文件。但事实上,这个根本就不是问题。文件多又没啥。有