在開發金流或第三方整合系統時,常會遇到這樣的狀況: 今天要支援 LinePay ,明天要加上 ApplePay ,後天可能還有 GooglePay 、PayPal …。
如果每增加一種支付方式都要打開主程式去修改,就好比一台遊戲機的遊戲被「寫死」在機器裡: 每次想玩新遊戲,就得整台機器重焊一次電路。 這樣不但麻煩,還很難維護。
更好的設計是:遊戲機本身保持穩定,玩家只要插上不同的卡帶,就能享受不同的遊戲 。 這就是 Plugin 架構(Plugin Architecture)+ 工廠(Factory)+ 策略模式(Strategy) 的精神。
它的優點在於:
彈性高 :不必動到遊戲機(主程式),就能插入新卡帶(Plugin)。
可測試性強 :每個卡帶都能單獨測試,不影響主機。
擴充容易 :只要遵守插槽規格,就能自由新增遊戲。
常見的應用像是:
金流模組(不同支付方式 = 不同遊戲卡帶)
外掛模組(Plugin System)
Middleware(專案套件)
第三方 API 整合(物流、支付、通訊)
🪵 Plugin Pattern(外掛模式) 主程式就像一台遊戲機,裡面不必寫死所有的邏輯,只要留好插槽,外部就能依需求插入不同的「遊戲卡帶」(Plugin)。
IPayable<TRequest, TResponse>
:定義插槽的規格(必須符合這個介面,才能順利插上去)。
LinePayPlugin
, ApplePayPlugin
:就像是不同的遊戲卡帶,各自帶有不同的內容與玩法。
這樣設計後,當要新增一個新的支付方式,就像多插入一片新的卡帶,主程式不用改動,仍然可以正常運作。
1 2 3 4 5 6 7 8 9 static object ResolvePlugin (string pluginKey ){ return pluginKey switch { "LINEPAY" => new LinePayPlugin(), "APPLEPAY" => new ApplePayPlugin(), _ => throw new Exception("Unknown plugin" ) }; }
真實應用中,可以使用 IServiceProvider.GetService(pluginType) 來注入 Plugin。
🪵 介面:插槽的規格 所有卡帶都必須符合這個插槽規格,否則遊戲機無法讀取。
1 2 3 4 public interface IPayable <TRequest , TResponse >{ Task<TResponse> Pay (TRequest request, Dictionary<string , string > headers, string method ) ; }
🪵 遊戲卡帶的資料模型 不同遊戲,自然會有各自的資料格式:
1 2 3 4 5 public class LinePayRequest { public string OrderId { get ; set ; } }public class LinePayResponse { public string Result { get ; set ; } }public class ApplePayRequest { public string Token { get ; set ; } }public class ApplePayResponse { public string Status { get ; set ; } }
🪵 卡帶實作 這裡有兩片卡帶,分別是 LinePay 與 ApplePay:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class LinePayPlugin : IPayable <LinePayRequest , LinePayResponse >{ public async Task<LinePayResponse> Pay (LinePayRequest request, Dictionary<string , string > headers, string method ) { Console.WriteLine($"[LinePay] OrderId: {request.OrderId} , Method: {method} " ); await Task.Delay(100 ); return new LinePayResponse { Result = "LinePay Success" }; } } public class ApplePayPlugin : IPayable <ApplePayRequest , ApplePayResponse >{ public async Task<ApplePayResponse> Pay (ApplePayRequest request, Dictionary<string , string > headers, string method ) { Console.WriteLine($"[ApplePay] Token: {request.Token} , Method: {method} " ); await Task.Delay(100 ); return new ApplePayResponse { Status = "ApplePay Success" }; } }
🪵 Factory Method Pattern(卡帶管理員模式) Plugin 很多,但要怎麼挑選?這時候就交給「工廠」來統一建立與管理。
當物件建立邏輯變得複雜或會根據條件改變時,我們不想讓主程式知道「怎麼 new 出一個物件」的細節,那就把 “怎麼建立的” 封裝在 Factory 裡,主程式只需給條件。我們「封裝了變化(encapsulate variation)」讓物件的建立方式隱藏起來,這樣如果將來物件建立方式要變,只有工廠改就好,不影響使用端。
遊戲機能插的卡帶越來越多,我們需要一個「卡帶管理員」來幫忙分類、快取。 這就是工廠方法的角色。這樣一來,遊戲機只要交代「我要玩哪一片卡帶」,工廠就會把卡帶準備好。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static ConcurrentDictionary<string , PluginMetadata> pluginMetadataCache = new ();static PluginMetadata GetOrCreatePluginMetadata (string key, Func<PluginMetadata> factory ){ return pluginMetadataCache.GetOrAdd(key, _ => factory()); } PluginMetadata metadata = GetOrCreatePluginMetadata(payTypeKey, () => { Type requestType = payTypeKey == "LINEPAY" ? typeof (LinePayRequest) : typeof (ApplePayRequest); Type responseType = payTypeKey == "LINEPAY" ? typeof (LinePayResponse) : typeof (ApplePayResponse); Type interfaceType = typeof (IPayable<,>).MakeGenericType(requestType, responseType); MethodInfo method = interfaceType.GetMethod("Pay" ); return new PluginMetadata(interfaceType, requestType, method); });
🪵 Reflection 遊戲機的讀卡頭 即使遊戲機不知道「卡帶裡面」具體的程式碼,只要知道它一定有 Pay 方法, 就能透過讀卡頭(Reflection)去呼叫正確的內容。
MethodInfo.Invoke(…) → 啟動遊戲(執行 Pay)。 MakeGenericType(…) → 動態生成正確型別。
這就是「延遲決策(Defer to Runtime)」的價值。
🪵 執行遊戲 到此我們已經準備好 Plugin 與實作細節,並且可以透過 key 與 jsonRequest, 取得執行付款所需要的物件,接下來就是執行了!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 object requestData = JsonSerializer.Deserialize(jsonRequest, metadata.RequestEntityType);object plugin = ResolvePlugin(payTypeKey); object [] parameters = new object [] { requestData, new Dictionary<string , string >(), "PayMethod" };Task task = (Task)metadata.MethodInfo.Invoke(plugin, parameters); await task;PropertyInfo resultProp = task.GetType().GetProperty("Result" ); object result = resultProp.GetValue(task);Console.WriteLine($"🔁 Plugin 執行結果: {JsonSerializer.Serialize(result)} " );
🪵 圖示 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 flowchart TD A[接收 payTypeKey + JSON request] --> B[取得 PluginMetadata] B --> B1{是否已快取?} B1 -- 是 --> C[使用快取的 PluginMetadata] B1 -- 否 --> D[執行 factory 建立 PluginMetadata] D --> E[建立 interfaceType] E --> F[建立 request/response type] F --> G[取得 MethodInfo(Pay 方法)] G --> H[建立 PluginMetadata 並加入快取] C --> I[反序列化 requestData] H --> I I --> J[取得 Plugin 實體] J --> K[執行 Plugin.Pay 方法(反射)] K --> L[等待任務完成(await)] L --> M[取得回傳結果 Task.Result] M --> N[輸出結果到 Console] %% 樣式區分(可選) style A fill:#E3F2FD,stroke:#90CAF9 style B fill:#FFF3E0,stroke:#FFB74D style C fill:#C8E6C9,stroke:#81C784 style D fill:#FFF3E0,stroke:#FFB74D style I fill:#F3E5F5,stroke:#BA68C8 style J fill:#E1F5FE,stroke:#4FC3F7 style K fill:#FFE0B2,stroke:#FF9800 style L fill:#D7CCC8,stroke:#A1887F style M fill:#D7CCC8,stroke:#A1887F style N fill:#DCEDC8,stroke:#AED581
🪵 結語 這個架構把「變化」收納進 Plugin,讓主流程保持穩定。
Plugin → 隨插隨用
Factory → 統一建立
Reflection → 延後決策
Strategy → 封裝行為