AOP 的目標#

Aspect-Oriented Programming (AOP) 的目標是減少處理橫切關注點 (Cross-Cutting Concerns) 時的重複程式碼。上一章展示了用 Decorator 來實現 Interception,但如果系統有數十個 Repository 介面,就需要為每個介面撰寫各自的 Decorator——這會產生大量 Boilerplate Code。

AOP 要解決的正是這個問題:如何只實作一次橫切關注點,就能套用到所有地方?

三種 AOP 方法#

方法優點缺點
SOLID 驅動(設計導向)不需額外工具,保留編譯期檢查,可維護性高需要重新設計既有介面,對 Legacy Code 較困難
Dynamic Interception容易加入,工具生態成熟依賴特定工具,失去編譯期支援
Compile-time Weaving對 Legacy Code 容易套用與 DI 理念背道而馳,限制多,依賴工具

本章的核心訊息是:透過良好的軟體設計(SOLID 原則),AOP 可以完全不依賴任何工具就實現。 這是作者最推薦的方法。

SOLID 原則與 Interception 的關係#

快速回顧 SOLID#

  • SRP (Single Responsibility Principle):每個類別只有一個改變的理由
  • OCP (Open/Closed Principle):對擴充開放,對修改封閉
  • LSP (Liskov Substitution Principle):子型別可以替換父型別
  • ISP (Interface Segregation Principle):介面應該小而專注
  • DIP (Dependency Inversion Principle):依賴於 Abstraction,而非具體實作

關鍵洞察#

當介面遵循 ISP 被縮小到極致,就會自然趨向泛型 Abstraction。一個只有一個方法的介面,可以被進一步抽象為泛型介面——這就是從設計中「長出」AOP 的方式。

從寬介面到泛型 Abstraction#

問題:寬介面#

傳統設計中,一個 Service 介面可能長這樣:

public interface IProductService
{
    Product GetById(int id);
    IEnumerable<Product> GetByCategory(int categoryId);
    void Update(Product product);
    void Delete(int id);
    void AdjustInventory(int productId, int quantity);
}

如果要為這個介面加上稽核 Decorator,必須為每個方法都實作攔截邏輯。更糟的是,每個類似的 Service 介面都需要各自的 Decorator。

Figure 10.1: 將 IProductService 分離為唯讀的 IProductQueryServices 與唯寫的 IProductCommandServices

解法:泛型 Command/Query Abstraction#

將操作拆分為獨立的 CommandQuery 物件,並透過泛型介面統一處理:

// 泛型 Command Handler
public interface ICommandHandler<TCommand>
{
    void Handle(TCommand command);
}

// 泛型 Query Handler
public interface IQueryHandler<TQuery, TResult>
{
    TResult Handle(TQuery query);
}

Figure 10.2: 包含七個成員的 IProductCommandServices 被替換為七個各含單一成員的介面

每個操作變成獨立的 Command 或 Query 物件:

public class AdjustInventoryCommand
{
    public int ProductId { get; set; }
    public int Quantity { get; set; }
}

public class AdjustInventoryCommandHandler
    : ICommandHandler<AdjustInventoryCommand>
{
    public void Handle(AdjustInventoryCommand command)
    {
        // 業務邏輯
    }
}

Figure 10.3: 透過將方法參數提取為 Parameter Objects,七個介面縮減為一個 ICommandService

泛型 Decorator:一次實作,全面套用#

有了泛型 Abstraction,一個 Decorator 就能套用到所有 Command 或 Query:

public class AuditingCommandHandler<TCommand>
    : ICommandHandler<TCommand>
{
    private readonly ICommandHandler<TCommand> decoratee;
    private readonly IAuditTrail auditTrail;

    public AuditingCommandHandler(
        ICommandHandler<TCommand> decoratee,
        IAuditTrail auditTrail)
    {
        this.decoratee = decoratee;
        this.auditTrail = auditTrail;
    }

    public void Handle(TCommand command)
    {
        auditTrail.Record(command);    // 記錄稽核
        decoratee.Handle(command);     // 委派執行
    }
}

AuditingCommandHandler<TCommand> 一個類別就取代了原本可能需要數十個專屬 Decorator 的工作。這就是泛型 Abstraction 的威力。

更多泛型 Decorator 範例#

同樣的模式可以套用到各種橫切關注點:

  • Transaction Decorator:在 Command 執行前開始交易,成功後 Commit,失敗後 Rollback
  • Security Decorator:檢查當前使用者對特定 Command 的執行權限
  • Validation Decorator:在執行前驗證 Command 物件的資料合法性
  • Logging Decorator:記錄 Command 的執行時間與結果

在 Composition Root 中組裝#

ICommandHandler<AdjustInventoryCommand> handler =
    new SecurityCommandHandler<AdjustInventoryCommand>(
        new TransactionCommandHandler<AdjustInventoryCommand>(
            new AuditingCommandHandler<AdjustInventoryCommand>(
                new AdjustInventoryCommandHandler(repository),
                auditTrail),
            transactionFactory),
        authService);

所有的橫切關注點都在 Composition Root 中以 Decorator 串接,業務邏輯的 Handler 完全不知道這些額外行為的存在。

Figure 10.4: 以 Auditing、Transaction 和 Security 等 Aspect 豐富實際的 Command Service

設計驅動 AOP 的效果#

採用這種方法後,整個系統的橫切關注點管理變成:

  • 一次實作:每個橫切關注點只需一個泛型 Decorator
  • 全面套用:在 Composition Root 中統一套用到所有 Command/Query
  • 編譯期安全:所有型別在編譯時就能檢查
  • 無需額外工具:純粹靠設計與語言特性實現
  • 易於測試:每個 Decorator 都可以獨立測試

這種方法需要從一開始就採用泛型 Abstraction 的設計方式。對於既有的 Legacy Code,重構成本可能較高,這也是為什麼第十一章會介紹工具導向的替代方案。

SOLID 原則如何驅動 AOP
SOLID 原則在 AOP 中的角色
SRP每個 Command Handler 只負責一項業務邏輯,每個 Decorator 只負責一項橫切關注點
OCP新增橫切關注點只需新增 Decorator,不修改既有程式碼
LSPDecorator 可以無縫替換原始 Handler
ISP將寬介面拆分為單一方法的泛型介面,使泛型 Decorator 成為可能
DIP所有 Handler 依賴於 ICommandHandler<T> Abstraction