概述#

當 SOLID 驅動的 AOP(第十章)因為 Legacy Code 或團隊因素而難以採用時,可以考慮工具導向的 AOP。本章介紹兩種方法:Dynamic Interception(動態攔截)Compile-time Weaving(編譯期織入),並分析各自的利弊。

作者明確表示偏好順序為:SOLID 驅動 AOP > Dynamic Interception > Compile-time Weaving。Compile-time Weaving 在本書中被視為反模式。

Dynamic Interception#

運作原理#

Dynamic Interception 透過函式庫在執行時期動態產生 Decorator,免去手動撰寫每個 Decorator 類別的工作。最知名的實作是 Castle Dynamic Proxy

Figure 11.1: Dynamic Interception 函式庫在執行時期動態產生 Decorator 類別

IInterceptor 介面#

Castle Dynamic Proxy 的核心是 IInterceptor 介面:

public interface IInterceptor
{
    void Intercept(IInvocation invocation);
}

所有攔截邏輯都實作在 Intercept 方法中,透過 IInvocation 物件存取被呼叫的方法資訊與參數。

Circuit Breaker 範例#

public class CircuitBreakerInterceptor : IInterceptor
{
    private readonly CircuitBreaker breaker;

    public CircuitBreakerInterceptor(CircuitBreaker breaker)
    {
        this.breaker = breaker;
    }

    public void Intercept(IInvocation invocation)
    {
        if (breaker.IsOpen)
            throw new CircuitBreakerOpenException();

        try
        {
            invocation.Proceed();  // 呼叫原始方法
            breaker.RecordSuccess();
        }
        catch (Exception ex)
        {
            breaker.RecordFailure();
            throw;
        }
    }
}

在 Composition Root 中使用#

var generator = new ProxyGenerator();
var interceptor = new CircuitBreakerInterceptor(breaker);

IProductRepository repository =
    generator.CreateInterfaceProxyWithTarget<IProductRepository>(
        new SqlProductRepository(connectionString),
        interceptor);

CreateInterfaceProxyWithTarget 會在執行時期產生一個實作 IProductRepository 的 Proxy 類別,自動將所有方法呼叫導向 Interceptor。

Figure 11.2: 客戶端呼叫被攔截的 Abstraction 時的方法呼叫流程

優點#

  • 減少 Boilerplate:不需要為每個介面手動撰寫 Decorator
  • 容易加入既有系統:不需要重構介面設計
  • 一個 Interceptor 套用多個介面:與泛型 Decorator 的效果類似

缺點#

  • 依賴特定工具:與 Castle Dynamic Proxy 或其他函式庫耦合
  • 執行期錯誤:型別錯誤在編譯時無法發現,延遲到執行時期才爆發
  • 脆弱的慣例:依賴字串比對方法名稱等慣例,容易因重構而破壞
  • 除錯困難:動態產生的 Proxy 讓 Call Stack 更難閱讀
  • 效能開銷:Reflection 與動態型別產生有額外成本

Dynamic Interception 是「次佳選擇 (Next best pick)」——當 SOLID 驅動的 AOP 對團隊來說變革太大時,Dynamic Interception 提供了一個務實的折衷方案。

Compile-time Weaving#

運作原理#

Compile-time Weaving 在編譯後修改 IL (Intermediate Language),將橫切關注點直接織入已編譯的程式碼中。最知名的工具是 PostSharp

Figure 11.3: Compile-time Weaving 流程

使用方式#

透過 Attribute 標記需要織入行為的方法:

[Transaction]
public void AdjustInventory(int productId, int quantity)
{
    // 業務邏輯
    // PostSharp 會在編譯後自動加入 Transaction 的開始與提交
}

Figure 11.4: Compile-time Weaving 視覺化

為什麼作者認為這是反模式#

作者對 Compile-time Weaving 的批評非常明確:

1. 與 DI 理念背道而馳

DI 的核心是在執行時期透過 Composition Root 組裝行為,而 Compile-time Weaving 在編譯時期就將行為烘焙進程式碼中,完全無法在部署時改變。

2. 違反 Open/Closed Principle

要改變一個方法的橫切行為,必須修改原始碼中的 Attribute 標記,而非透過擴充來改變。

3. 違反 Dependency Inversion Principle

Aspect 類別(如 [Transaction] 的實作)無法使用 Constructor Injection,因為它們的實例由框架在編譯時期織入,不經過 Composition Root。

4. 測試困難

  • 無法輕易用 Test Double 替換 Aspect 的行為
  • Aspect 的邏輯與業務邏輯緊密耦合在同一個編譯單元中
  • 整合測試變得更複雜

5. SOLID 驅動的方法在各方面都更優秀

比較維度SOLID 驅動Compile-time Weaving
編譯期型別安全有限
可測試性
靈活度執行期可調整編譯時固定
工具依賴
Constructor Injection完整支援不支援
除錯體驗正常困難

Compile-time Weaving 表面上降低了程式碼量,但代價是失去了 DI 帶來的所有彈性——鬆耦合、可測試性、執行期組裝能力。作者認為這個取捨不值得。

三種 AOP 方法的選擇指引#

AOP 方法選擇決策流程
  1. 能否採用泛型 Abstraction 重構介面?

    • 是 → 使用 SOLID 驅動的 AOP(最佳選擇)
    • 否 → 進入下一步
  2. 系統是否為 Legacy Code 且重構成本過高?

    • 是 → 使用 Dynamic Interception(務實的折衷)
    • 否 → 考慮漸進式重構,逐步走向 SOLID 驅動
  3. 是否有極端的限制使得 Dynamic Interception 也無法使用?

    • 極少數情況下才考慮 Compile-time Weaving
    • 即便如此,也應視為過渡方案而非最終架構
本章關鍵術語
  • Dynamic Interception:透過函式庫在執行時期動態產生 Proxy/Decorator
  • Compile-time Weaving:在編譯後修改 IL,將橫切邏輯織入程式碼
  • Castle Dynamic Proxy:.NET 生態系中最常用的 Dynamic Interception 函式庫
  • PostSharp:.NET 生態系中最知名的 Compile-time Weaving 工具
  • IInterceptor:Castle Dynamic Proxy 中定義攔截邏輯的介面
  • IInvocation:代表被攔截的方法呼叫,包含方法資訊與參數