#分享 給用 Laravel 中小企業的架構建議:別用 Service + Repository,改用 Mutation + Query

近期工作的小感想 --- 國內外的 Laravel 社群,主流的架構建議是使用 Service 類別和 Repository 類別來組織原始碼。 然而,我發現許多團隊在實施這個架構時會遇到兩個問題。 第一個問題是,隨著時間推移,程式碼會散落各處,無法明確區分哪些應該放在 Service、哪些應該放在 Repository、哪些應該放在 Controller。邊界變得模糊,導致整體可維護性下降。 第二個問題是,「檔案數量」的成長速度趕不上「程式碼數量」的成長速度。 如果你的 Service、Repository 數量是每個月增加兩三個,但是你程式碼行數是每個星期增加兩三百行,那開發兩三年之後當然會有檔案肥大的問題。 舉例來說,如果你們公司有串金流功能的話,那我猜你的 OrderService 類別應該就超過一千行了,然後跟這個類別有關的程式碼也變得很難維護。 所以我認為 Service 加 Repository 這個架構並不適合用在 Laravel 開發。 --- 最近在設計新架構時,我受到 GraphQL 的啟發,試著把每件事單獨放在一個 Mutation 或 Query 中,結果出乎意料地好。 今天想與大家分享這種「扁平式」新架構:Laravel Flat Architecture (注意:此架構受到 GraphQL Query/Mutation 概念的啟發,但它不是 GraphQL) ## Laravel Flat Architecture - 兼顧開發速度與可維護性 這個架構非常簡單,我們只需要安裝
接著在 app 資料夾建立 Mutations 和 Queries 資料夾。 在開發新功能時: - 如果會更新到資料庫,就新增一個 Mutation 類別。 - 如果不會更新到資料庫,就新增一個 Query 類別。 - Controller 只負責呼叫各種 Mutation 或 Query。 我直接把我一個專案(藝文活動搜集行事曆)的部分檔案開源給大家參考: - [yii.tw/app/Mutations]
- [yii.tw/app/Queries]
當你這樣整理程式碼,Controller 就會變得非常簡潔優雅: - [yii.tw/app/Http/Controllers/MyEventController.php]
``` class MyEventController extends Controller { function index(Request $request) { $events = \App\Queries\GetUserEventsWithPagination::run(Auth::user()->id); return view('my-events.index', compact('events')); } function edit($id, Request $request) { $event = \App\Queries\GetEvent::run($id); if ($event->user->id != Auth::user()->id) { abort(403); } return view('add-event', compact('event')); } function update($id, Request $request) { $event = \App\Queries\GetEvent::run($id); if ($event->user->id != Auth::user()->id) { abort(403); } $imageFileName = null; if ($request->hasFile('image')) { $imageFileName = handleImage($request); } $event = \App\Mutations\UpdateEvent::run( event: $event, title: $request->input('name'), description: $request->input('description'), city: $request->input('city'), url: $request->input('url'), placeKey: $request->input('place_name'), category: $request->input('category'), fromDate: $request->input('from'), toDate: $request->input('to'), inDates: explode(',', $request->get('in_dates')), imageFileName: $imageFileName, ); return redirect()->to('/my-events')->with('status', '活動修改成功'); } function destroy($id, Request $request) { $event = \App\Queries\GetEvent::run($id); if ($event->user->id != Auth::user()->id) { abort(403); } \App\Mutations\DeleteEvent::run($event); return redirect()->to('/my-events')->with('status', '活動刪除成功'); } } ``` 更多範例檔案與說明,可以參考這邊:
這個架構雖然看起來簡單,但其實有很多好處。 ## 1. 讓 Junior Developer 也能輕鬆上手 團隊有初級開發者時,他們通常會有過度抽象化、做出錯誤抽象化的情況出現,讓專案越來越難維護。 Laravel Flat Architecture 會讓專案呈現扁平,Mutations 與 Queries 內可能有數百個檔案,但每個檔案不會超過兩三百行。 這樣的架構避免了 God Object 這種反模式,開發速度快且抽象化簡單。 ## 2. 讓 Code Review 變簡單 在審查同事的 PR 做 Code Review 時也很簡單,只要看看 Controller 內容是否單純優雅即可。 如果 Controller 呼叫了 Eloquent 預設的 save()、delete()、where() 等邏輯,就表示邏輯洩漏到 Controller 裡了。 這時請對方重構到 Mutations/Queries 類別即可。 ## 3. 設計自動化測試變簡單 採用這個扁平架構,會讓自動化測試的檔案結構同時變得很簡單。 一個 Mutation 對應一個測試、一個 Query 對應一個測試即可! 此外,你還可以寫一個簡單的覆蓋率測試。 無論使用 PHPUnit 還是任何工具都可以輕鬆實現。 只需要掃描 Mutations 與 Queries 資料夾,確認每個檔案在 tests 資料夾中是否有對應的測試檔案即可。 ## 4. 讓自動化架構檢查成為可能 同事發的 PR 就算能用,通常在抽象化設計與架構方面需要跟大家一起討論、沒辦法自動化架構檢查。 然而 Laravel Flat Architecure 這個架構因為非常簡單,因此你可以在本地端或 CI/CD 環境中使用工具檢查它。 舉例來說,我這邊附上一份 PHPStan 的設定檔,裡面會檢查你的 Controller 是否出現 save、delete、where,如果有就會擋掉。 - [phpstan.neon]
``` includes: - vendor/larastan/larastan/extension.neon - vendor/spaze/phpstan-disallowed-calls/extension.neon parameters: level: 0 paths: - app/Http/Controllers disallowedMethodCalls: - method: Illuminate\Database\Eloquent\Model::save message: "❌ 禁止在 Controller 內直接使用 save(),請使用 Mutations!" - method: Illuminate\Database\Eloquent\Model::update message: "❌ 禁止在 Controller 內直接使用 update(),請使用 Mutations!" - method: Illuminate\Database\Eloquent\Model::delete message: "❌ 禁止在 Controller 內直接使用 delete(),請使用 Mutations!" - method: Illuminate\Database\Eloquent\Builder::where message: "❌ 禁止在 Controller 內直接使用 where(),請使用 Queries!" - method: Illuminate\Database\Eloquent\Builder::find message: "❌ 禁止在 Controller 內直接使用 find(),請使用 Queries!" - method: Illuminate\Database\Eloquent\Builder::first message: "❌ 禁止在 Controller 內直接使用 first(),請使用 Queries!" - method: Illuminate\Database\Eloquent\Builder::get message: "❌ 禁止在 Controller 內直接使用 get(),請使用 Queries!" ``` 如果新同事不清楚架構,可以將這個工具裝到 CI/CD 裡,這樣他們在 push 時就會亮紅燈,無法 merge。 ## 結語 為了避免檔案過於肥大,我認為採用扁平化的架構比較好,而且這種架構不會妨礙進一步的抽象化。 例如,如果多個 Mutations 有共用邏輯,可以使用 OOP 方式建一個 Parent 類別,讓其他 Mutation 繼承它。若喜歡 Composition,也可以使用 Trait 將某些動作單獨成檔案,讓其他 Mutation 直接 use 也可以。 總而言之,在規格不斷變化、急著開發新功能、沒時間深入設計抽象化時,我鼓勵使用扁平的架構來當第一步。然後在規格明確、心有餘力的時候,再好好設計更進階的抽象化,這樣更有利於長期開發。 --- 我之後預計會分享關於 Laravel Flat Architecture 的更多內容,有興趣者可以參考:
--- 原文發表在:
megapx
愛心
哈哈
19
7
全部留言