分離式客製化開發:兼顧模組化與客製化的 Laravel 彈性架構

接案的時候,常常很想把一套系統寫完之後,一口氣賣給多個客戶。

偏偏每個客戶都有各自不同的客製化需求,沒辦法真的都拿同一個專案去佈署。

只好把一個專案複製成好幾份,然後分別客製化給不同客戶。

但這樣又會出現維護問題:各專案的差異越變越大。

之後就算更新了核心功能、修理了核心 bug,也很難一口氣替所有客戶升級。

如果各專案的差異是無法避免的,那有沒有辦法至少,讓核心系統與客製化的程式碼分開來放呢?

最好能讓各專案的檔案結構都像這樣:

System/
Customization/

如此一來,雖然 Customization/ 內的 code 不能重複使用,至少 System/ 內的 code 可以。

可以將 System/ 內的 code 更新進其他專案,也可以拿強化後的核心系統去接未來的新案子。

我把這種檔案結構稱為「分離式客製化架構」,下文用 SCA (Separated Customization Architecture) 代稱。

這篇文章跟大家分享在 Laravel 底下如何使用這種 SCA 架構。

前置需求

使用 SCA 架構非常簡單,只要安裝 laravel-modules 套件就可以了。

laravel-modules 套件非常單純,它只是運用了 Laravel 原生的 Package Development 觀念,幫助你把 app/ 與 resources/ 資料夾底下的程式碼移進另外的 Modules/ 資料夾而已。

Modules/ 內可以有一或多個資料夾,每個資料夾就跟一個獨立 laravel app 一樣,有自己的 controllers, views, entities, routes, migrations… 等等內容。

laravel-modules 套件就只是幫你讀取各個模組的內容而已,沒有其他黑魔法。

如此一來,你的 Laravel 專案的檔案結構會變成這樣:

Modules/
app/
resources/
...

注意到了嗎?它剛好對應到前面說的檔案結構:

System/
Customization/

Modules/ 就是 System/,而 app/ 與 resources/ 就是 Customization/。

那麼實際上該如何開發呢?以下直接舉例說明。

情境:你開發了一套 Awesome 通用系統來接案

首先你根據過去的經驗開發了一套 Awesome 電商官網模組,打算用它來接許多案子,賺大錢。

你的 laravel 檔案結構長這樣:

Modules/Awesome/
app/
resources/

Awesome 模組的 controllers, views, entities, routes, migrations… 等等內容都在 Modules/Awesome/ 裡面,就跟一個獨立 laravel app 一樣。

而 app 與 resources 資料夾內目前只有 laravel 預設檔案,你沒寫半行 code 在裡面。

你拿 Awesome 系統開始接案,接著客戶出現。他們有以下的客製化需求。

情境:客戶想修改 UI

客戶要求對首頁畫面微調。

這個時候請利用 laravel 原生的 Overriding Package Views 機制,你直接新增一個檔案:

resources/views/vendor/awesome/homepage.blade.php

就在這個檔案內隨心所欲的重寫或是亂寫吧!它會直接取代原本的這個檔案:

Modules/Awesome/Resources/views/homepage.blade.php

如果不只是畫面微調,這種模板覆蓋不夠用,那就可以視為修改系統的 business logic,請接著往下讀。

情境:客戶想修改系統的 business logic

客戶想修改結帳前的購物車頁面與功能。

此功能原本的 routing 定義在 Modules/Awesome/Http/routes.php 內:

Route::group(['middleware' => 'web', 'namespace' => 'Modules\Awesome\Http\Controllers'], function()
{
    Route::get('/shopping/cart', 'ShoppingController@cart');
    // blah blah...
});

請在原本 laravel 預設的 routes/web.php 內新增:

Route::get('/shopping/cart', 'ShoppingController@cart');

接著建立 app/Http/Controllers/ShoppingController.php 檔案,去繼承原先在 Modules/Awesome/ 內的 controller:

namespace App\Http\Controllers;

use Modules\Awesome\Http\Controllers\ShoppingController as BaseController;
use Illuminate\Http\Request;

class ShoppingController extends BaseController
{
    function cart(Request $request)
    {
        // 隨你修改商業邏輯 ...

        // 繼續用原有模板與覆蓋機制
        // return view('awesome::shopping.cart');

        // 或是在 resources/views/ 內建一個新的也行
        return view('the-new-cart');
    }
}

這樣就會直接取代原本的功能了!

最棒的是,這只是用上了基本的 OOP 類別繼承觀念,以及 laravel 的特性:「重複的話,後定義的 route 會覆蓋先定義的 route」。

有需要的話,domain model 內的類別也可以用同樣手法繼承來擴充。

比如說原本有個 Modules/Awesome/Cart.php 類別:

namespace Modules\Awesome;

use Illuminate\Database\Eloquent\Model;

class Cart extends Model
{
    // blah ...
}

你可以建立 app/Cart.php 這樣的子類別:

namespace App;

use Modules\Awesome\Cart as BaseCart;

class Cart extends BaseCart
{
    // blah ...
}

在新建立的 controller 內就可以使用這些擴充後的新 domain model 類別。

用這種手法可以自由新增程式碼!而且全是在 app/ 或 resources/ 資料夾內新增,完全沒碰到 Modules 內的東西。

情境:客戶想開發新功能

開發全新功能就更簡單了,直接在 routes/web.php 內增加 routing 後,在 app/ 與 resources/ 內增加你需要的檔案就行了。

就跟平常開發 laravel 專案一樣!

有需要的話,就在新的檔案裡面引入或是繼承 Modules/ 內的類別就行了。

情境:需要更新資料庫 schema

laravel-modules 有提供指令幫忙產生 migration 檔案。

所以屬於 Awesome 系統的 migration 檔案會在 Modules/Awesome/Database/Migrations/ 底下。

如果客製化時需要更新 schema ,就用 laravel 原生的 migration 指令在 database/migrations/ 內建立 migration file 即可。

連 migration file 都可以完全分離在 System/ 與 Customization/ 裡面!

結論

SCA 的特色在於它不預設你打算把專案模組化或是客製化到哪種程度。

你可以為了客戶在 Customization/ 內先寫完功能。之後有空再完全重構或是部份重構進 System/。

也可以遇到需求就先強化 System/,寫到不適合的地方再開始從 Customization/ 接手客製化。

你可以視情況來來回回,以一種混合的方式寫 code。

你可以邊寫邊決定,事後隨時反悔也沒問題,所以 SCA 非常適合接案類型的軟體開發。

針對 SCA 還有許多進階議題可以討論,本文只談最基本的觀念與技巧,但應該足以解決大部份的問題。

我自己用這種方式接案開發了幾個月,效果非常不錯,分享給各位參考。

有任何問題歡迎留言讓我知道,或是寄信到 [email protected] 與我討論。

(完)