Yu Wu Hsien - Profile Picture
YU WU HSIEN

Just simple folk, with HTML, trying to make a living.

重構(一):從單體應用到模組化

五年前,為了支援公司創業初期的營運需求,我使用 Laravel 開發了一個庫存管理系統。初期的功能範圍明確:進貨管理、庫存追蹤、訂單處理。借助 Laravel 的快速開發能力,系統很快就投入使用並獲得認可。

Laravel 的美好就在於此 — 它讓你可以快速將想法變成可運行的產品,並且在早期就可以得到相當多的回饋。

然而,隨著業務的發展,系統需求持續擴張。五年來,陸續加入了多租戶管理、藥品批號追蹤、供應商管理、第三方發票整合、進銷報表等功能,並逐步整合了藥局門市通路、電商購物車、POS 系統等多個子系統,這使得系統越來越大,也越來越複雜。

今天,我面對的是一個包含數十個業務模組、數百個類別檔案的大型應用程式,以及散落的數個子系統。

困境

目錄結構

最初,系統遵循 Laravel 的標準目錄結構組織檔案。這種按類型分類的方式在專案初期運作良好:

app/
├── Http/Controllers/
├── Models/
├── Jobs/
└── Listeners/

但隨著業務模組增加,問題開始逐漸浮現。以採購模組為例,相關的程式碼分散在多個目錄:

app/Http/Controllers/PurchaseOrderController.php
app/Models/PurchaseOrder.php
app/Jobs/ProcessPurchaseOrder.php
app/Listeners/PurchaseOrderCreated.php
app/Notifications/PurchaseOrderNotification.php

同一個業務模組的程式碼分散在五個(或更多)不同的目錄中。

更進一步觀察 app/Models/ 目錄:

app/Models/
├── User.php
├── Order.php
├── Invoice.php
├── PurchaseOrder.php
├── ScrapOrder.php
├── Stock.php
├── Product.php
└── ...(30+ 個 Models)

這些 Model 除了都屬於同一類型之外,在業務邏輯上並無關聯。InvoiceScrapOrder 被放在同一目錄,僅僅因為它們都是 Model,但實際上分屬不同的業務領域。

子系統的維護

隨著業務擴展,我們發展出三個子系統:

  • 管理後台:供內部人員管理庫存與訂單
  • 電商平台:提供客戶線上購物功能
  • 門市 POS:處理實體門市的銷售作業

這些子系統最初是獨立的專案,透過 API 進行溝通。雖然將部分共用邏輯抽取為獨立的 Package,但在時間壓力與持續增長的業務需求下,仍經常出現重複實作的情況:

php
// 主系統
class InventoryService {
    public function calculateStock($productId) {
        // 庫存計算邏輯
    }
}

// POS 系統
class PosInventoryService {
    public function calculateStock($productId) {
        // 相似的庫存計算邏輯
    }
}

// 電商系統
class CommerceInventoryService {
    public function calculateStock($productId) {
        // 再次實作相似邏輯
    }
}

相同的業務邏輯在不同系統中重複實作,即使它們只有小部分差異,也增加了維護成本與出錯機率。

嘗試

我曾嘗試在各類型目錄下建立業務子目錄:

app/
├── Http/Controllers/
│   ├── Inventory/
│   ├── Procurement/
│   └── Order/
├── Models/
│   ├── Inventory/
│   ├── Procurement/
│   └── Order/
└── Jobs/
    ├── Inventory/
    ├── Procurement/
    └── Order/

這種方式確實改善了檔案的組織,但在實際開發時,仍然需要在多個類型目錄間切換才能完成一個功能的修改,例如:要修改採購功能時:

Http/Controllers/Procurement/PurchaseOrderController.php
Models/Procurement/PurchaseOrder.php
Jobs/Procurement/PurchaseOrderCreated.php
Listeners/Procurement/PurchaseOrderCreated.php
Notifications/Procurement/PurchaseOrderNotification.php

我還是在多個目錄間穿梭,只是現在每個目錄都多了一層。

問題

這讓我開始思考:一定要按類型來組織程式碼嗎?

在開發特定業務功能時,我想要關注的是 業務邏輯的完整性,而非 Controller、Model、Job 等類型的區分。如果能將同一業務模組的所有相關類別集中在同一個目錄下,開發時就能更專注於該模組本身。

如果可以不需要按類型來組織程式碼,那麼一定要嚴格遵循完整的 領域驅動設計(DDD),實作 Bounded Context、Aggregate Root、Domain Events 等概念才可以嗎?

有什麼方式可以在實務需求與架構複雜度之間取得平衡的折衷方案?

業界實踐

在尋找解決方案的過程中,我發現許多開發者都遇到了類似的問題。

Jeffrey WayTwitter 上分享的經驗印證了我的想法:

A really useful technique for slightly larger projects is to build and organize app features as if you were making a Composer package. Group all of your related models, controllers, listeners, commands, etc. under the same directory and namespace.

他建議將相關的 Models、Controllers、Listeners、Commands 等都組織在同一個目錄和命名空間下,就像開發 Composer Package 那樣。

此外,Mateus GuimarãesModularizing the monolith: a real-world experience 中分享了他的實際經驗,而 Shopify 工程團隊也在 Deconstructing the Monolith 中討論了類似的架構演進。

這些實踐經驗讓我更確信:按業務領域組織程式碼是更合理的選擇。

調整

整體結構

基於以上的想法,我調整為以下的架構:

modules/
├── src/
│   ├── Apps/              # 應用程式層
│   │   ├── Admin/         # 管理後台
│   │   ├── Store/         # 門市系統
│   │   └── Commerce/      # 電商平台
│   ├── Core/              # 核心共用邏輯
│   │   ├── User/
│   │   └── Organization/
│   ├── Customer/          # 客戶管理
│   ├── Procurement/       # 採購管理
│   ├── Inventory/         # 庫存管理
│   ├── Finance/           # 財務管理
│   └── Infrastructure/    # 基礎設施
├── config/                # 模組配置檔
├── database/              # 資料庫遷移
├── routes/                # 路由定義
├── resources/             # 視圖與資源
└── tests/                 # 測試檔案

新的架構下,configdatabaseroutes 等目錄並非放在各個子系統下,而是統一在 modules 根目錄。主要原因是這些目錄管理的是 跨模組的共用資源

  • database/migrations:所有模組的資料表變更需要統一管理版本
  • routes/:雖然按子系統分檔,但統一放置便於查看整體路由結構
  • config/:模組層級的配置,各子系統透過 Service Provider 存取
  • tests/:測試策略統一管理,但仍按模組組織

統一管理共用資源,同時保持模組獨立,在實務上取得了很好的平衡。

模組結構

每個業務模組內部組織所有相關的類別。以採購模組為例:

modules/src/Procurement/
├── Http/
│   └── Controllers/
│       ├── PurchaseOrderController.php
│       └── ReplenishmentController.php
├── Jobs/
│   └── FilterReplenishmentCandidate.php
├── Listeners/
│   └── LogReplenishmentHistory.php
├── Models/
│   ├── PurchaseOrder.php
│   └── PurchaseOrderItem.php
├── Enums/
│   ├── ReplenishmentReason.php
│   └── ReplenishmentStatus.php
└── ReplenishmentManager.php

使用時的命名空間也更加明確:

php
// 過去
use App\Models\PurchaseOrder;

// 現在
use Modules\Procurement\PurchaseOrder;

從命名空間即可清楚看出 PurchaseOrder 屬於採購模組,且相關的所有類別都可在 Procurement 目錄下找到。 這種組織方式讓開發時能夠專注於單一業務領域,而不需要在多個類型目錄間來回切換。

子系統架構

各子系統統一放置在 modules/src/Apps/ 下:

modules/src/Apps/
├── Admin/
│   ├── Http/
│   ├── Models/
│   ├── Actions/
│   └── AdminServiceProvider.php
├── Store/
│   ├── Http/
│   ├── Models/
│   └── StoreServiceProvider.php
└── Commerce/
    ├── Http/
    ├── Models/
    └── CommerceServiceProvider.php

每個子系統透過自己的 Service Provider 管理路由、認證、設定等,彼此保持獨立。同時,它們可以:

  • 共用核心模組的業務邏輯
  • 繼承並擴展既有功能
  • 在需要時覆寫特定實作

所有系統在同一個專案中維護,有效避免了程式碼重複。

Composer 設定

composer.json 中註冊模組的命名空間:

json
{
  "autoload": {
    "psr-4": {
      "App\\": "app/",
      "Modules\\": "modules/src/"
    }
  },
  "autoload-dev": {
    "psr-4": {
      "Tests\\": "tests/",
      "Modules\\Tests\\": "modules/tests/"
    }
  }
}

效益

開發效率提升

調整前:修改採購功能需要在多個目錄間切換

app/Http/Controllers/...
app/Models/...
app/Jobs/...
app/Listeners/...

調整後:所有相關檔案集中在單一目錄

modules/src/Procurement/

程式碼重用

調整前:各子系統獨立維護相似邏輯

php
Admin/InventoryService.php
Store/InventoryService.php
Commerce/InventoryService.php

調整後:共用核心模組

php
// modules/src/Inventory/InventoryService.php
namespace Modules\Inventory;

class InventoryService { ... }

// 各子系統直接使用
use Modules\Inventory\InventoryService;

擴充性

需要新增物流系統時,只需在 modules/src/Apps/ 下建立新目錄:

modules/src/Apps/
└── Logistics/
    ├── Http/
    ├── Models/
    └── LogisticsServiceProvider.php

新系統可立即重用所有現有的業務模組邏輯。

最後

從標準的 Laravel 目錄結構演進到模組化架構,是一段漫長但值得的過程。因為這中間還必須兼顧公司的營運、增加新功能、調整既有功能等,而不是一件可以不受干擾的任務在進行。

重構的最佳時機往往比我們想像的晚。在系統建構過程中,我們會持續加深對業務領域的理解。在缺乏領域知識的情況下就設計複雜的系統架構或微服務,只會增加不必要的風險與成本。

David Heinemeier HanssonThe Majestic Monolith 中也探討了這個議題。他並非反對良好的軟體實踐,而是提醒我們:不要花費數週甚至數月的時間,去設計一個尚未充分理解的系統,尤其當它可能始終只是一個小型專案時。

架構是演進的結果,而非預先設計的完美藍圖。

適合的架構方案取決於團隊規模、應用程式複雜度,以及實際的運作情況。無論選擇哪種架構,在演變過程中都會經歷一段痛苦和掙扎的時期。但當發現開發效率下降、修改系統變得困難時,也許就是該重新思考的時候了。

這次的重構讓我親身經歷了完整的循環:體會單體架構的便利、察覺它逐漸成為瓶頸、最終思考並實施改變。這不是來自上層的決策,而是基於實際痛點的深刻體會。

重構的目標不在於追求理論上的完美架構,而是 讓程式碼更容易理解、維護與擴展。沒有絕對最好的架構,只有最適合當前情境與團隊的架構。

© 2025 Yu Wu Hsien.