在《JUnit: A Cook's Tour》一文中,Erich Gamma 與 Kent Beck 深入剖析了 JUnit 的設計哲學。他們指出,TestCase 這一關鍵抽象擁有極高的「模式密集」—— 易於使用卻難以修改。本文作為 AOP@Work 系列第四期,將重溫這場經典探討,展示如何透過面向切面編程(AOP)的切入點設計,取代傳統物件導向設計,從而在滿足主要目標的同時,不再被迫犧牲次要設計目標,延長系統壽命。
即使最優秀的 Java 程式也會隨時間老化。為了滿足新需求,設計不斷演進,關鍵物件承擔越來越多模式角色,最終變得難以使用、難以修改,迫使團隊重構或重寫。Gamma 與 Beck 在 Cook's Tour 中遵循標準設計流程:發現關鍵抽象、封裝為物件、添加模式安排角色與服務。不幸的是,這正是導致模式密集的根源——關鍵抽象的職責與關係不斷膨脹,直到像步入中年的父母只能按老習慣行事,一旦需求超出能力便陷入困境。

Gamma 與 Beck 為了讓測試易於操作,採用 Command 模式將請求封裝為物件,要求開發者必須繼承 TestCase 並實作 runTest() 方法。這雖然提高了可用性,卻強迫客戶端遵循特定協議。
AOP 提供了另一種抽象方式:切入點。切入點不關心程式碼放在哪個類別的哪個方法,只宣告「讓某個連接點成為測試案例」。例如:
pointcut testCase() : execution(void Runnable+.run());
pointcut testCase() : execution(static void main(String[]));
pointcut testCase() : execution(public void Test+.run(TestResult));
pointcut testCase() : execution(public void TestCase+.test*());
只要測試能被切入點選中,即可成為測試案例,即使它原本不是為測試而寫。這將可用性從「API 契約」擴展為一個連續體:從介面契約、容器程式模型,到 AOP 的多種形式。開發者無需了解服務細節即可獲得服務,如同人民依賴政府規則般自然。
JUnit 使用 Template Method 模式封裝「建立夾具、執行測試、清理夾具」的流程,runBare() 強制子類別遵循固定協議。AspectJ 則使用 around 通知:
Object around() : testCase() {
setup(thisJoinPoint);
try {
return proceed(); // 繼續執行測試連接點
} finally {
cleanup(thisJoinPoint);
}
}
這提供了三大自由度:支援任意連接點、不假設執行程式碼型態、夾具管理可覆寫或委託。通知之間互不可見,僅綁定於宣告的連接點上下文,極大降低了交互複雜度。
JUnit 以 TestResult 為收集參數,導致「簽名污染」與調用順序鎖死(TestCase 與 TestResult 相互調用)。AspectJ 以 ThreadLocal 儲存 TestContext,配合 around 通知獨立處理結果收集:
void around() : testCase() {
ITest test = wrap(getTest(thisJoinPoint));
TestContext ctx = TestContext.getTestContext(test);
ctx.startTest(test);
try {
proceed();
ctx.endTest(test);
} catch (ResultReported e) {
ctx.checkReported(test);
} catch (Error e) {
ctx.testError(test, null, e);
} catch (Throwable e) {
ctx.testFailure(test, null, e);
}
}
通過優先級控制(declare precedence),可決定夾具錯誤是否作為測試錯誤報告,且無需修改核心類別。這消除了 JUnit 中「捆綁」帶來的剛性——使用者不再被迫接受或拒絕整包服務。
JUnit 用 Pluggable Selector 解決「類別膨脹」,但 TestCase 成為 Adapter,導致 TestSuite 緊耦合反射協議。AspectJ 直接在連接點配置測試:
void around(Printer printer) : testCase() && context(printer) {
for (Printer p : Printer.findPrinters()) {
p.setMode(Printer.MONOCHROME); proceed(p);
p.setMode(Printer.COLOR); proceed(p);
}
}
或使用 IConfigurable 介面實現泛化配置。關鍵在於:方面可透過 declare parents 為目標類別動態實作介面,且成員可設為方面私有,避免污染目標類別的命名空間。
JUnit 採用 Composite 模式組合測試套件,導致 TestSuite 深度依賴 TestCase。AspectJ 以 IComposite 標記介面 + 遞歸通知取代:
void around(IComposite c) : recursing(c) {
recurseTop(c, new IClosure() {
public void runNext(IComposite next) { proceed(next); }
});
}
IClosure 封裝 proceed(),結合 Command 模式的參數傳遞優勢與通知的上下文隱藏優勢,且不要求目標類型實作特定介面,結合性大幅提升。
本文展示的所有面向切面方案共同特徵:
模式密集是 OO 設計的自然產物,但並非不可避免。AOP 讓我們在實現主要目標時,不再被迫放棄次要目標。這不僅是技術選擇,更是設計哲學的轉變——在連接點上封裝行為,而非在類別中堆疊角色。
AspectJ 1.0 發布多年,多數開發者停留在「模組化追蹤、同步等橫切關注點」的入門階段。真正的「全速 AOP」意味著:
思維陷阱在於:以為 AOP 只是收集分散代碼,或認為 OO 已解決所有問題。本文證明,即使代碼看似已良好模組化,AOP 仍能避免開發者默認接受的設計權衡。希望更多開發者嘗試「全速 AOP」,在更多應用中釋放設計靈活性。