Laravel 單行為控制器設(shè)計(jì)的魅力

Laravel 單行為控制器設(shè)計(jì)的魅力

昨天,Jeffrey Way 發(fā)布了一條推文,他問(wèn)大家更愿意將其控制器命名為單數(shù)還是復(fù)數(shù)。 我回答我兩種方案都不選,我使用單動(dòng)作控制器。隨后發(fā)生的是,有的人同意,有的不同意,有的甚至做出了最奇怪的事情。

由于十分強(qiáng)烈的反映,我想寫(xiě)一篇文章來(lái)解釋為什么我愛(ài)單行為控制器、還有我為什么覺(jué)得它們很美妙。

首先在開(kāi)始文章之前,我想要說(shuō)這個(gè)東西并不是只有單一的真相。與往常一樣,我想指出的是,一切都?xì)w結(jié)于你的個(gè)人喜好。我只能教、建議和指出一些事情,由你來(lái)決定是否同意、不同意、接受、學(xué)習(xí)和 / 或調(diào)整?;蛘叨疾皇?。從這篇博客中獲得你想要的,隨心所欲地做讓自己感到舒適的事情吧。

對(duì)比 CRUD 和 Domain Modelling

開(kāi)始前,我們先來(lái)想想我們傾向于寫(xiě) resourceful 的 CRUD 控制器。我相信很多人會(huì)堅(jiān)持使用這種做法,因?yàn)檫@是 laravel 中的一個(gè)標(biāo)準(zhǔn)做法,文檔中的大多數(shù)示例也是使用這種方法。另外,這或許也是你在各類(lèi)博客或 app 代碼中經(jīng)常看到的。

但是,如果你停下來(lái)思考一下,這是編寫(xiě)它們的最佳方法嗎?是軟件行業(yè)的一般性做法嗎?最近幾年,我在 “領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)”(Domain Driven Design) 等領(lǐng)域投入了大量時(shí)間,并且思考軟件如何應(yīng)用于你工作的領(lǐng)域(Domian)以及它轉(zhuǎn)化的過(guò)程。當(dāng)您開(kāi)始考慮模仿您領(lǐng)域中無(wú)處不在的語(yǔ)言的術(shù)語(yǔ)和措辭時(shí),您會(huì)發(fā)現(xiàn)您的代碼將變得更加清晰明了,更加戳到點(diǎn)子上。(最后這一句話(huà)仍值得斟酌、改進(jìn))

最后,我相信編寫(xiě)軟件的本質(zhì)是盡可能地應(yīng)用 domain processes 來(lái)讓你的代碼更加易讀和更加可維護(hù)。

Resourceful 控制器在這兩個(gè)方面做得并不好。首先,它們不易讀,因?yàn)槟鷥A向于根據(jù)數(shù)據(jù)來(lái)構(gòu)建它們,而不是根據(jù)領(lǐng)域來(lái)構(gòu)建它們。這樣的話(huà),你就會(huì)丟失上下文對(duì)照。你表現(xiàn)了數(shù)據(jù)的處理方式,但卻沒(méi)有說(shuō)明到底發(fā)生什么了,也沒(méi)有說(shuō)明你使用哪個(gè)過(guò)程進(jìn)行處理。

第二,你沒(méi)有針對(duì)可維護(hù)性進(jìn)行優(yōu)化。由于你是根據(jù)數(shù)據(jù)結(jié)構(gòu)來(lái)構(gòu)建的,因此你也會(huì)跟著耦合進(jìn)去。實(shí)際上,您的領(lǐng)域模型在不斷發(fā)展,數(shù)據(jù)結(jié)構(gòu)也在不斷發(fā)展。如果你的數(shù)據(jù)結(jié)構(gòu)處理著多個(gè)過(guò)程或領(lǐng)域的多個(gè)部分,那你將很難進(jìn)行調(diào)整。

一個(gè)實(shí)際的例子

因?yàn)槔碚摵軣o(wú)聊,上代碼更加容易解釋?zhuān)晕覀儊?lái)看一個(gè)實(shí)際的例子。

假設(shè)您正在構(gòu)建一個(gè)應(yīng)用,它允許用戶(hù)去組織事件。您想提供一種創(chuàng)建,更新和刪除這些事件的方法。這是一種非常典型的例子,你會(huì)用 CRUD 的方式來(lái)考慮實(shí)現(xiàn)它。那么,讓我們看看就這樣一個(gè) resourceful 控制器是如何被轉(zhuǎn)換的。

首先我們來(lái)看看路由:

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']);

現(xiàn)在對(duì)應(yīng)的控制器:

<?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)     {         // ...     } }

這個(gè) EventController 處理所有的 CRUD 請(qǐng)求,展示事件列表,展示指定的事件,創(chuàng)建一個(gè)事件,更新一個(gè)現(xiàn)存的事件和刪除一個(gè)事件。

來(lái)看看 index 方法的細(xì)節(jié):

public?function?index() { ????$events?=?Event::paginate(10); ????return?view('events.index',?compact('events')); }

在這個(gè)方法中,我們檢索出事件們,然后交給視圖讓它去展示到一個(gè)分頁(yè)列表中。 到目前為止都還好。但是你現(xiàn)在想實(shí)現(xiàn)一個(gè)方法,用不同的頁(yè)面去查看過(guò)去和即將來(lái)的事件。讓我們看看如何在 index 方法中實(shí)現(xiàn)它:

public?function?index(Request?$request) { ????if?($request-&gt;boolean('past'))?{ ????????$events?=?Event::past()-&gt;paginate(10); ????}?elseif?($request-&gt;boolean('upcoming'))?{ ????????$events?=?Event::upcoming()-&gt;paginate(10); ????}?else?{ ????????$events?=?Event::paginate(10); ????} ????return?view('events.index',?compact('events')); }

呃?。】雌饋?lái)好亂啊。盡管我們已經(jīng)用 Eloquent scopes 來(lái)隱藏查詢(xún)邏輯,但是還是有很丑的鏈?zhǔn)秸Z(yǔ)句。我們來(lái)看看如何用單行為控制器來(lái)代替它。

每個(gè)單行為控制器只執(zhí)行一件事情,僅僅一件事情。

首先,我們不使用查詢(xún)參數(shù)去獲得不同的事件列表,而是使用專(zhuān)用路由去實(shí)現(xiàn)它。

Route::get('events',?ShowAllEventsController::class); Route::get('events/past',?ShowPastEventsController::class); Route::get('events/upcoming',?ShowUpcomingEventsController::class);

這個(gè)路由比之前的要長(zhǎng)一些,但是這個(gè)比之前的要更有表達(dá)力。你可以一下子辨識(shí)出哪一個(gè)控制器處理哪一個(gè)特定的邏輯。如果你對(duì)比一下 URL,你會(huì)看到在可讀性上改進(jìn)了一些:

#?Before /events /events?past=true /events?upcoming=true #?After /events /events/past /events/upcoming

現(xiàn)在來(lái)看其中一個(gè)控制器。就看 ShowUpcomingEventsController 這個(gè)控制器:

<?php namespace AppHttpControllers; use AppModelsEvent; final class ShowUpcomingEventsController {     public function __invoke()     {         $events = Event::upcoming()->paginate(10); ????????return?view('events.index',?compact('events')); ????} }

丑陋的 if 語(yǔ)句沒(méi)了, 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.

簡(jiǎn)單,易讀,便于維護(hù)。

你可能會(huì)問(wèn)自己,這樣做值么,畢竟之前的 if 語(yǔ)句也沒(méi)那么壞吧?但是我想向你展示的是你正在為未來(lái)的改進(jìn)做優(yōu)化,并改進(jìn)維護(hù)性。下次你想要對(duì)這三個(gè)頁(yè)面做任何指定改變的時(shí)候,你會(huì)知道在哪里改,并且不需要艱難地更新一個(gè) if 語(yǔ)句。

當(dāng)然,上面的例子很簡(jiǎn)單,我們來(lái)看一個(gè)更復(fù)雜一點(diǎn)的。我們?cè)囋?a href="http://m.babyishan.com/tag/%e9%87%8d%e6%9e%84">重構(gòu) create 和 store 方法:

public?function?create() { ????return?view('events.create'); } public?function?store(Request?$request) { ????$data?=?$request-&gt;validate([ ????????'name'?=&gt;?'required', ????????'start'?=&gt;?'required', ????????'end'?=&gt;?'required|after:start', ????]) ????$event?=?Event::create($data); ????return?redirect()-&gt;route('event.show',?$event); }

我們要做的就是把這兩個(gè)方法移到專(zhuān)用的控制器,這樣更好地解釋了這些方法做了啥。這些方法更好地服務(wù)于你,比起把它們放在一個(gè)叫做 ScheduleNewEventController 的控制器中。我們接著更新這個(gè)控制器的路由:

Route::get('events/schedule',?[ScheduleNewEventController::class,?'showForm']); Route::post('events/schedule',?[ScheduleNewEventController::class,?'schedule']);

我不會(huì)向你展示一個(gè)確切的控制器,因?yàn)樗鼈冇泻蜕厦娴睦右粯樱袃蓚€(gè)方法,只不過(guò)把 showForm 和 schedule 重新命名為更能表達(dá)它們干了啥的名字。即使這個(gè)不是單行為控制器,但是方法論是一樣的:把你應(yīng)用中的專(zhuān)用行為(方法)和它對(duì)應(yīng)的控制器拆分到一起。

好了,現(xiàn)在你已經(jīng)看了單行為控制器的例子了。你可能會(huì)想,這會(huì)導(dǎo)致越來(lái)越多的文件。但事實(shí)上,這個(gè)根本就不是問(wèn)題。文件多又沒(méi)啥。有更多、更小、更容易維護(hù)的文件比有更大、更難分析的要好。你可以打開(kāi)一個(gè)單行為控制器的文件,然后快速掃描代碼,馬上就能知道這是干嘛的。

我經(jīng)常把他們分組到不同的目錄,這些目錄負(fù)責(zé)領(lǐng)域的各個(gè)部分。這讓你從文件結(jié)構(gòu)的角度看控制器時(shí),更加容易。

拆分控制器也讓你跟容易找到特定的一個(gè)控制器。想象一下,你要尋找那個(gè)可以安排事件的控制器時(shí)。現(xiàn)在你只需要按照文件名搜索編輯器,而不是一個(gè)通用的 EventController。

其他情況

我也被問(wèn)到是否要對(duì)所有控制器執(zhí)行此操作。不總是。在命名控制器時(shí),我傾向于嚴(yán)謹(jǐn)且簡(jiǎn)潔,但我也會(huì)像你一樣適應(yīng)各種情況。

當(dāng)然,有時(shí)候你還是想用 resourceful 控制器。比如在你構(gòu)建 restful API 時(shí)。這樣做是很有意義,因?yàn)槟憬?jīng)常直接與數(shù)據(jù)本身交互,而沒(méi)有經(jīng)常與領(lǐng)域或任何進(jìn)程進(jìn)行交互。cms(內(nèi)容管理系統(tǒng))或 Laravel Nova 等應(yīng)用程序就是最好的例子。

但是在需要的時(shí)候,您最好問(wèn)問(wèn)自己的方案是否更接近領(lǐng)域和處理過(guò)程。在需要根據(jù)領(lǐng)域執(zhí)行操作的時(shí)候,比如 graphql 之類(lèi)的或 API 之類(lèi)的 rpc ,這樣做可能更適合。

結(jié)論

我希望這有一點(diǎn)見(jiàn)地,你現(xiàn)在能更理解我為什么如此喜歡單行為控制器了吧。我相信,結(jié)合小的 classes,再使用無(wú)處不在的語(yǔ)言、顯式地命名,會(huì)帶來(lái)更可維護(hù)的代碼,甚至是控制器,不僅僅是領(lǐng)域對(duì)象。但是正如我開(kāi)頭所說(shuō),選擇能幫助你的部分,好好分辨哪些適用于你,哪些不行。

推薦教程:《PHP教程》《PHP教程

? 版權(quán)聲明
THE END
喜歡就支持一下吧
點(diǎn)贊12 分享
站長(zhǎng)的頭像-小浪學(xué)習(xí)網(wǎng)月度會(huì)員