跳到主要内容

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. 程式碼糾纏:商業邏輯被基礎設施程式碼淹沒(1 行商業邏輯 vs. 19 行基礎設施程式碼)
  2. 程式碼散佈:相同的日誌記錄/快取/授權模式在每個服務方法中重複出現
  3. 維護夢魘:更改日誌格式需要修改每一個方法
  4. 違反單一職責原則:每個方法處理多個職責
  5. 容易出錯:新方法容易忘記加入日誌記錄或授權

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 的特定弱點:

面向OOPAOP
模組化單元ClassAspect
擅長處理核心商業邏輯、資料建模、封裝橫跨多個 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
MethodInvocationOverrideMethodAspect[Log][Cache][Authorize]
PropertyAccessOverrideFieldOrPropertyAspect[NotifyPropertyChanged][Validate]
FieldAccessOverrideFieldOrPropertyAspect[Audit](追蹤欄位變更)
ConstructorExecutionAspect + IInitializableAdvice[InjectDependency][Singleton]
EventHandlingOverrideEventAspect[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 解析AspectAspectClass discovery(掃描 assembly 中所有繼承自 Aspect 的型別)
2. Pointcut 匹配Pointcut + Join PointAttribute 標註、FabricsEligibility 規則計算出最終的 Joinpoint 集合
3. Advice 織入AdviceOverrideMethod() / OverrideProperty() 等 template 產生程式碼,以 Roslyn 改寫 IL

整個流程完全發生在編譯期:執行期只是執行普通 C#,沒有反射、沒有代理

「編譯時期」在實務上的意義

  1. 你的原始碼保持乾淨——你只需撰寫 Attribute 和商業邏輯
  2. 編譯器產生基礎設施程式碼——日誌記錄、快取、授權等
  3. 沒有執行期反射或代理——產生的程式碼就是普通的 C# 方法呼叫
  4. 錯誤在建置時期捕捉——如果 Aspect 套用不正確,你會得到編譯器錯誤
  5. 你可以檢視產生的程式碼——查看 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 可介入的點
AdviceAspect 在 Join Point 執行的程式碼
Pointcut選擇要套用哪些 Join Point 的規則
Weaving將 Aspect 與原始程式碼結合
編譯時期 Weaving在編譯期間進行 Weaving(Metalama 的做法)

下一章快速開始——安裝 Metalama 並建立你的第一個 Aspect。