Aspect-Oriented Programming 基礎
在深入 Metalama 之前,必須先了解它所解決的問題,以及它所屬的程式設計典範:Aspect-Oriented Programming (AOP)。
問題:橫切關注點
在任何非簡易的應用程式中,某些行為需要出現在程式碼的許多部分。這些被稱為橫切關注點(cross-cutting concerns),因為它們「橫切」了程式碼正常的模組邊界。
常見的橫切關注點
| 關注點 | 範例 |
|---|---|
| 日誌記錄 | 記錄每個方法的進入/離開及參數 |
| 快取 | 快取耗時方法的回傳值 |
| 授權 | 在方法執行前檢查使用者權限 |
| 驗證 | 驗證方法參數(非空、範圍內) |
| 例外處理 | 暫態失敗時重試、斷路器模式 |
| 稽核 | 記錄誰在何時做了什麼,以符合法規遵循 |
| 效能 | 測量執行時間、節流、防抖 |
| 執行緒 | 同步存取、分派至 UI 執行緒 |
| 交易管理 | 將操作包裝在資料庫交易中 |
傳統 OOP 的問題
考慮一個簡單的服務方法,需要日誌記錄、授權、快取和計時:
// ❌ Without AOP — cross-cutting concerns dominate the business logic
public async Task<Recipe> GetRecipeAsync(int id)
{
// Authorization (4 lines)
var user = _currentUserService.GetCurrentUser();
if (user == null || !user.IsAuthenticated)
throw new UnauthorizedException("Must be authenticated");
// Logging (2 lines)
_logger.LogDebug("Entering GetRecipeAsync with id={Id}", id);
// Timing (1 line)
var stopwatch = Stopwatch.StartNew();
try
{
// Caching (6 lines)
var cacheKey = $"Recipe_{id}";
var cached = await _cacheService.TryGetAsync<Recipe>(cacheKey);
if (cached != null)
return cached;
// ✅ Actual business logic (1 line!)
var recipe = await _repository.GetByIdAsync(id);
// More caching (1 line)
await _cacheService.SetAsync(cacheKey, recipe, TimeSpan.FromMinutes(5));
// More logging (1 line)
_logger.LogDebug("Exiting GetRecipeAsync, returning {Recipe}", recipe);
return recipe;
}
catch (Exception ex)
{
// Exception logging (2 lines)
_logger.LogError(ex, "Error in GetRecipeAsync");
throw;
}
finally
{
// More timing (2 lines)
stopwatch.Stop();
_logger.LogDebug("GetRecipeAsync took {Elapsed}ms", stopwatch.ElapsedMilliseconds);
}
}
這種做法的問題:
- 程式碼糾纏:商業邏輯被基礎設施程式碼淹沒(1 行商業邏輯 vs. 19 行基礎設施程式碼)
- 程式碼散佈:相同的日誌記錄/快取/授權模式在每個服務方法中重複出現
- 維護夢魘:更改日誌格式需要修改每一個方法
- 違反單一職責原則:每個方法處理多個職責
- 容易出錯:新方法容易忘記加入日誌記錄或授權
AOP 的解決方案
使用 AOP 後,同樣的方法變成:
// ✅ With AOP — clean, focused business logic
[Authorize]
[Log]
[Cache(AbsoluteExpirationSeconds = 300)]
[Timing]
public async Task<Recipe> GetRecipeAsync(int id)
{
return await _repository.GetByIdAsync(id);
}
橫切關注點現在以 Attribute 宣告式地表達,框架負責將它們織入實際的執行流程中。
什麼是 Aspect-Oriented Programming?
Aspect-Oriented Programming (AOP) 是一種程式設計典範,透過提供模組化橫切關注點的方式,來補充 Object-Oriented Programming (OOP)。
核心概念
AOP 讓你能夠一次定義橫切行為,將其放在一個稱為 Aspect 的單一位置,然後宣告式地套用到程式碼中任意數量的目標。
AOP vs. OOP
AOP 不是 OOP 的替代品。它與 OOP 並行運作,以解決 OOP 的特定弱點:
| 面向 | OOP | AOP |
|---|---|---|
| 模組化單元 | Class | Aspect |
| 擅長處理 | 核心商業邏輯、資料建模、封裝 | 橫跨多個 Class 的橫切關注點 |
| 分解方式 | 垂直(按領域實體/服務) | 水平(按關注點,橫跨所有實體) |
| 程式碼重用 | 繼承、組合、介面 | 宣告式套用 Aspect |
| 程式碼執行時機 | 明確的方法呼叫 | 由框架隱式織入 |
可以這樣理解:
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Service A │ │ Service B │ │ Service C │
│ │ │ │ │ │
─────────┼──────────┼──┼──────────┼──┼──────────┼──── Logging (aspect)
│ │ │ │ │ │
─────────┼──────────┼──┼──────────┼──┼──────────┼──── Authorization (aspect)
│ │ │ │ │ │
─────────┼──────────┼──┼──────────┼──┼──────────┼──── Caching (aspect)
│ │ │ │ │ │
└──────────┘ └──────────┘ └──────────┘
← OOP (vertical) →
← AOP (horizontal, cross-cutting) →
AOP 術語
理解 AOP 需要學習幾個關鍵術語:
Aspect
Aspect 是封裝橫切關注點的模組化單元。它定義了做什麼以及在哪裡做。
在 Metalama 中,Aspect 是一個繼承自基底類別(例如 OverrideMethodAspect)的 C# 類別:
public class LogAttribute : OverrideMethodAspect
{
public override dynamic? OverrideMethod()
{
Console.WriteLine($"Entering {meta.Target.Method.Name}");
var result = meta.Proceed();
Console.WriteLine($"Exiting {meta.Target.Method.Name}");
return result;
}
}
Join Point
Join Point 是程式執行過程中一個明確定義的點,Aspect 可以在此處被套用。範例:
- 方法執行
- 屬性存取(get/set)
- 欄位存取
- 建構函式執行
- 例外處理
在 Metalama 中,Join Point 由你使用的 Aspect 基底類別類型決定。下圖是 AOP Join Point 的概念分類,每個分類對應 Metalama 一個(或一組)Override*Aspect 基底類別:
| Join Point 分類 | 對應 Metalama 基底類別 | 典型 Aspect |
|---|---|---|
| MethodInvocation | OverrideMethodAspect | [Log]、[Cache]、[Authorize] |
| PropertyAccess | OverrideFieldOrPropertyAspect | [NotifyPropertyChanged]、[Validate] |
| FieldAccess | OverrideFieldOrPropertyAspect | [Audit](追蹤欄位變更) |
| ConstructorExecution | Aspect + IInitializableAdvice | [InjectDependency]、[Singleton] |
| EventHandling | OverrideEventAspect | [ThreadSafeEvent] |
Advice
Advice 是 Aspect 在 Join Point 執行的實際程式碼。它定義了 Aspect 被觸發時會發生什麼事。
Metalama 中的 Advice 類型:
| Advice 類型 | 說明 | 範例 |
|---|---|---|
| Before | 在原始程式碼之前執行的程式碼 | 授權檢查 |
| After | 在原始程式碼之後執行的程式碼 | 記錄回傳值 |
| Around | 包裝原始程式碼的程式碼(前 + 後) | 重試、計時、快取 |
| Introduction | 加入目標型別的新成員 | 新增 INotifyPropertyChanged |
Pointcut
Pointcut 定義 Aspect 應該被套用到哪些 Join Point。在 Metalama 中,Pointcut 透過以下方式定義:
- Attribute:將
[Log]套用到特定方法 - Fabrics:使用類似 LINQ 的查詢以程式化方式選擇目標
- Eligibility:定義 Aspect 可以被套用到哪些宣告
Weaving
Weaving 是將 Aspect 與原始程式碼結合的過程。這是各 AOP 框架之間差異最大的地方:
| Weaving 策略 | 時機 | 框架範例 | 優點 | 缺點 |
|---|---|---|---|---|
| 編譯時期 | 編譯期間 | Metalama、PostSharp | 無執行期開銷、提早捕捉錯誤、可除錯 | 需要建置工具 |
| 執行期(代理) | 執行時期透過代理 | Castle DynamicProxy、DispatchProxy | 設定簡單、無需更改建置 | 執行期開銷、僅限 virtual/interface 方法 |
| 執行期(IL) | 執行時期透過 IL 改寫 | Harmony、MonoMod | 可修改任何程式碼 | 脆弱、難以除錯、執行期開銷 |
| Source Generation | 編譯期間 | C# Source Generators | 屬於 Roslyn 的一部分、無需外部工具 | 無法修改既有程式碼、只能新增程式碼 |
Metalama 使用編譯時期 Weaving,這表示你的 Aspect 在 C# 編譯過程中被套用。編譯器輸出的 IL 已經包含 Aspect 邏輯——沒有執行期開銷。
編譯時期 Weaving 詳解
這是理解 Metalama 最重要的概念。下圖將 Metalama 從原始碼到最終 Binary 的 Weaving lifecycle 拆成三個編譯期階段(Aspect 解析 → Pointcut 匹配 → Advice 織入),對應 §AOP 術語中的三大概念:
各階段的對照:
| 階段 | 對應 AOP 術語 | Metalama 內部負責的元件 |
|---|---|---|
| 1. Aspect 解析 | Aspect | AspectClass discovery(掃描 assembly 中所有繼承自 Aspect 的型別) |
| 2. Pointcut 匹配 | Pointcut + Join Point | Attribute 標註、Fabrics、Eligibility 規則計算出最終的 Joinpoint 集合 |
| 3. Advice 織入 | Advice | 由 OverrideMethod() / OverrideProperty() 等 template 產生程式碼,以 Roslyn 改寫 IL |
整個流程完全發生在編譯期:執行期只是執行普通 C#,沒有反射、沒有代理。
「編譯時期」在實務上的意義
- 你的原始碼保持乾淨——你只需撰寫 Attribute 和商業邏輯
- 編譯器產生基礎設施程式碼——日誌記錄、快取、授權等
- 沒有執行期反射或代理——產生的程式碼就是普通的 C# 方法呼叫
- 錯誤在建置時期捕捉——如果 Aspect 套用不正確,你會得到編譯器錯誤
- 你可以檢視產生的程式碼——查看
obj/<Config>/<TFM>/metalama/下的檔案
視覺化轉換過程
編譯前(你的原始碼):
[Log]
public int Add(int a, int b)
{
return a + b;
}
編譯後(實際執行的程式碼,可在 obj/.../metalama/ 中查看):
public int Add(int a, int b)
{
Console.WriteLine("Entering Add(a = {0}, b = {1})", a, b);
try
{
int result;
result = a + b;
Console.WriteLine("Exiting Add with result = {0}", result);
return result;
}
catch (Exception ex)
{
Console.WriteLine("Error in Add: {0}", ex.Message);
throw;
}
}
為什麼 AOP 對 GST 框架很重要
GST 框架大量使用 AOP,因為它所服務的領域:
1. FDA 21 CFR Part 11 法規遵循
製藥與醫療器材製造需要完整的稽核軌跡。每次資料變更都必須追蹤:
- 誰做了變更
- 何時做的
- 變更前/後的值是什麼
- 為什麼(變更原因)
沒有 AOP,每個修改資料的方法都需要 10 行以上的稽核程式碼。使用 AOP 後:
[Part11Audit]
[AuditDataChange]
[RequireChangeReason]
public async Task UpdateRecipe(Recipe recipe) { ... }
2. 工業設備通訊
SECS/GEM 和 Modbus 協定需要健全的錯誤處理:
- 暫態通訊失敗時重試
- 持續性失敗的斷路器
- 逾時管理
- 串列埠的執行緒同步
3. WPF 桌面應用程式
工廠現場應用程式需要:
- 數百個 ViewModel 屬性的
INotifyPropertyChanged - 從背景執行緒分派至 UI 執行緒
- 屬性變更追蹤以支援復原/重做
4. 效能監控
即時製造系統需要:
- 執行時間監控
- 快速感測器資料的節流/防抖
- 常用設定的快取
AOP 讓 GST 框架能夠將所有這些功能以簡單的宣告式 Attribute 提供,任何應用程式都可以使用,而無需理解底層的複雜性。
摘要
| 概念 | 定義 |
|---|---|
| AOP | 模組化橫切關注點的程式設計典範 |
| Aspect | 封裝橫切關注點的可重用模組 |
| Join Point | 程式碼中 Aspect 可介入的點 |
| Advice | Aspect 在 Join Point 執行的程式碼 |
| Pointcut | 選擇要套用哪些 Join Point 的規則 |
| Weaving | 將 Aspect 與原始程式碼結合 |
| 編譯時期 Weaving | 在編譯期間進行 Weaving(Metalama 的做法) |
下一章:快速開始——安裝 Metalama 並建立你的第一個 Aspect。