阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

63 個專案實戰,寫出作品集,讓面試官眼前一亮!

立即開始免費試讀!

國內外的 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 - 兼顧開發速度與可維護性

這個架構非常簡單,我們只需要安裝 https://github.com/lorisleiva/laravel-actions

接著在 app 資料夾建立 Mutations 和 Queries 資料夾。

在開發新功能時:

  • 如果會更新到資料庫,就新增一個 Mutation 類別。
  • 如果不會更新到資料庫,就新增一個 Query 類別。
  • Controller 只負責呼叫各種 Mutation 或 Query。

我直接把我一個專案(藝文活動搜集行事曆)的部分檔案開源給大家參考:

當你這樣整理程式碼,Controller 就會變得非常簡潔優雅:

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', '活動刪除成功');
    }
}

更多範例檔案與說明,可以參考這邊:https://github.com/howtomakeaturn/laravel-flat-architecture

這個架構雖然看起來簡單,但其實有很多好處。

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,如果有就會擋掉。

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 的更多內容,有興趣者可以參考:https://codelove.tw/@howtomakeaturn/post/qBgOdx

按讚的人:

共有 0 則留言


👉 身份:資深全端工程師、指導過無數人半路出家轉職 👉 使命:打造 CodeLove 成為優質新手村,讓非本科也有地方自學&討論

阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

63 個專案實戰,寫出作品集,讓面試官眼前一亮!

立即開始免費試讀!