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#
將操作拆分為獨立的 Command 和 Query 物件,並透過泛型介面統一處理:
// 泛型 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,不修改既有程式碼 |
| LSP | Decorator 可以無縫替換原始 Handler |
| ISP | 將寬介面拆分為單一方法的泛型介面,使泛型 Decorator 成為可能 |
| DIP | 所有 Handler 依賴於 ICommandHandler<T> Abstraction |