昨天,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 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('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 App\Http\Controllers; use App\Models\Event; 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 having 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 重新命名为更能表达它们干了啥的名字。即使这个不是单行为控制器,但是方法论是一样的:把你应用中的专用行为(方法)和它对应的控制器拆分到一起。
好了,现在你已经看了单行为控制器的例子了。你可能会想,这会导致越来越多的文件。但事实上,这个根本就不是问题。文件多又没啥。有更多、更小、更容易维护的文件比有更大、更难分析的要好。你可以打开一个单行为控制器的文件,然后快速扫描代码,马上就能知道这是干嘛的。
我经常把他们分组到不同的目录,这些目录负责领域的各个部分。这让你从文件结构的角度看控制器时,更加容易。
拆分控制器也让你跟容易找到特定的一个控制器。想象一下,你要寻找那个可以安排事件的控制器时。现在你只需要按照文件名搜索编辑器,而不是一个通用的 EventController。
其他情况
我也被问到是否要对所有控制器执行此操作。不总是。在命名控制器时,我倾向于严谨且简洁,但我也会像你一样适应各种情况。
当然,有时候你还是想用 resourceful 控制器。比如在你构建 RESTful API 时。这样做是很有意义,因为你经常直接与数据本身交互,而没有经常与领域或任何进程进行交互。CMS(内容管理系统)或 Laravel Nova 等应用程序就是最好的例子。
但是在需要的时候,您最好问问自己的方案是否更接近领域和处理过程。在需要根据领域执行操作的时候,比如 GraphQL 之类的或 API 之类的 RPC ,这样做可能更适合。
结论
我希望这有一点见地,你现在能更理解我为什么如此喜欢单行为控制器了吧。我相信,结合小的 classes,再使用无处不在的语言、显式地命名,会带来更可维护的代码,甚至是控制器,不仅仅是领域对象。但是正如我开头所说,选择能帮助你的部分,好好分辨哪些适用于你,哪些不行。
以上是Laravel 單一行為控制器設計的魅力的詳細內容。更多資訊請關注PHP中文網其他相關文章!