在開發金流或第三方整合系統時,常會遇到這樣的狀況:
今天要支援 LinePay,明天要加上 ApplePay,後天可能還有 GooglePayPayPal…。

如果每增加一種支付方式都要打開主程式去修改,就好比一台遊戲機的遊戲被「寫死」在機器裡:
每次想玩新遊戲,就得整台機器重焊一次電路。
這樣不但麻煩,還很難維護。

更好的設計是:遊戲機本身保持穩定,玩家只要插上不同的卡帶,就能享受不同的遊戲
這就是 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

// 反序列化 request data
object requestData = JsonSerializer.Deserialize(jsonRequest, metadata.RequestEntityType);

// 執行 Plugin 的 Pay 方法
object plugin = ResolvePlugin(payTypeKey); // 拿到 plugin instance
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 → 封裝行為