在xUnit
實(shí)現(xiàn)模式中,存在TestCase, TestSuite, TestResult, TestListener, TestMethod
等重要領(lǐng)域?qū)ο蟆?/p>
重構(gòu)之前
在我實(shí)現(xiàn)Cut
的最初的版本中填帽,TestSuite
與TestResult
之間的關(guān)系是緊耦合的荣堰,并且它們的職責(zé)分配也不合理匙铡。
Cut是一個(gè)使用Modern C++實(shí)現(xiàn)的xUnit框架蒋搜。
TestSuite: 持有Test實(shí)例集的倉庫
TestSuite
是一個(gè)持有Test
實(shí)例列表的倉庫矮湘,它持有std::vector<Test*>
類型的實(shí)例集。它實(shí)現(xiàn)了Test
接口荧琼,并覆寫了run
虛函數(shù)。此外差牛,在實(shí)現(xiàn)run
時(shí)命锄,提取了一個(gè)私有函數(shù)runBare
。
// cut/core/test_suite.h
#include <vector>
#include "cut/core/test.h"
struct TestSuite : Test {
~TestSuite();
void add(Test* test);
private:
void run(TestResult& result) override;
private:
void runBare(TestResult& result);
private:
std::vector<Test*> tests;
};
TestSuite
維護(hù)了Test
實(shí)例的生命周期偏化,初始時(shí)為空脐恩,并通過add
接口添加Test
類型的動(dòng)態(tài)實(shí)例;最后侦讨,通過析構(gòu)函數(shù)回收所有的Test
實(shí)例驶冒。
void TestSuite::add(Test* test) {
tests.push_back(test);
}
TestSuite::~TestSuite() {
for (auto test : tests) {
delete test;
}
}
inline void TestSuite::runBare(TestResult& result) {
for (auto test : tests) {
test->run(result);
}
}
void TestSuite::run(TestResult& result) {
result.startTestSuite(*this);
runBare(result);
result.endTestSuite(*this);
}
TestResult: 測試結(jié)果的收集器
TestResult
的職責(zé)非常簡單,作為Test
的聚集參數(shù)韵卤,用于搜集測試結(jié)果骗污。它持有TestListener
實(shí)例集,當(dāng)測試執(zhí)行至關(guān)鍵階段沈条,將測試的狀態(tài)和事件通知給TestListener
需忿。TestListener
監(jiān)聽TestResult
的狀態(tài)變化,通過定制和擴(kuò)展實(shí)現(xiàn)測試數(shù)據(jù)統(tǒng)計(jì)蜡歹、測試進(jìn)度上報(bào)屋厘、測試報(bào)表生成等特性。
struct TestResult {
~TestResult();
void add(TestListener*);
void startTestSuite(const Test& test);
void endTestSuite(const Test& test);
private:
template <typename Action>
void boardcast();
private:
std::vector<TestListener*> listeners;
};
TestResult
維護(hù)了TestListener
實(shí)例集的生命周期月而。初始時(shí)該集合空汗洒,通過add
接口添加TestListener
類型的動(dòng)態(tài)實(shí)例;最后景鼠,通過析構(gòu)函數(shù)回收所有的TestListener
實(shí)例仲翎。
另外痹扇,TestResult
為TestSuite
公開了兩個(gè)事件處理接口startTestSuite, endTestSuite
。需要特別注意的是溯香,私有的函數(shù)模板boardcast
并沒有在頭文件中實(shí)現(xiàn)鲫构,它在實(shí)現(xiàn)文件中內(nèi)聯(lián)實(shí)現(xiàn),其消除了重復(fù)的迭代邏輯玫坛。
void TestResult::add(TestListener* listener) {
listeners.push_back(listener);
}
template <typename Action>
inline void TestResult::boardcast(Action action) {
for (auto listener : listeners) {
action(listener);
}
}
TestResult::~TestResult() {
boardcast([](auto listener) {
delete listener;
});
}
void TestResult::startTestSuite(const Test& test) {
boardcast([&test](auto listener) {
listener->startTestSuite(test);
});
}
void TestResult::endTestSuite(const Test& test) {
boardcast([&test](auto listener) {
listener->endTestSuite(test);
});
}
職責(zé)分布不合理
如下圖所示结笨,TestSuite::run
方法依賴于TestResult
的兩個(gè)公開成員函數(shù)startTestSuite, endTestSuite
。
觀察TestSuite::run
的實(shí)現(xiàn)邏輯湿镀,其與TestResult
關(guān)系更加緊密炕吸。因?yàn)椋?code>TestSuite::run調(diào)用TestSuite::runBare
前后兩個(gè)語句分別調(diào)用了TestResult
的兩個(gè)成員函數(shù)TestResult::startTestSuite, TestResult::endTestSuite
完成的。與之相反勉痴,TestSuite::runBare
則與TestSuite
更加緊密赫模,因?yàn)樗枰闅v私有數(shù)據(jù)成員tests
。
據(jù)此推論蒸矛,TestSuite::run
的實(shí)現(xiàn)邏輯與TestResult
關(guān)系更加密切瀑罗,應(yīng)該將相應(yīng)的代碼搬遷至TestResult
。難點(diǎn)就在于雏掠,runBare
在中間斩祭,而且又與TestSuite
更為親密,這給重構(gòu)帶來了挑戰(zhàn)乡话。
搬遷職責(zé)
重構(gòu)TestResult
既然TestSuite::run
的實(shí)現(xiàn)邏輯相對于TestResult
更加緊密摧玫,應(yīng)該將其搬遷至TestResult
。經(jīng)過重構(gòu)绑青,TestResult
公開給TestSuite
唯一的接口為runTestSuite
诬像,而將startTestSuite, endTestSuite
私有化了。
struct TestResult {
// ...
void runTestSuite(TestSuite&);
private:
void startTestSuite(const Test& test);
void endTestSuite(const Test& test);
private:
std::vector<TestListener*> listeners;
};
void TestResult::runTestSuite(TestSuite& suite) {
startTestSuite(suite);
suite.runBare(*this);
endTestSuite(suite);
}
重構(gòu)TestSuite
不幸的是时迫,TestSuite
也因此必須公開runBare
接口颅停。
struct TestSuite : Test {
// ...
void runBare(TestResult& result);
private:
void run(TestResult& result) override;
private:
std::vector<Test*> tests;
}
void TestSuite::runBare(TestResult& result) {
for(auto test : tests) {
test->run(result);
}
}
void TestSuite::run(TestResult& result) {
result.runTestSuite(*this);
}
// ...
經(jīng)過一輪重構(gòu),TestSuite
雖然僅僅依賴于TestResult::runTestSuite
一個(gè)公開接口掠拳,但TestResult
也反向依賴于TestSuite::runBare
癞揉,依賴關(guān)系反而變成雙向依賴,兩者之間的耦合關(guān)系更加緊密了溺欧。
但本輪重構(gòu)是具有意義的喊熟,經(jīng)過重構(gòu)使得TestSuite
與TestResult
的職責(zé)分布更加合理,唯一存在的問題就是兩者之間依然保持緊耦合的壞味道姐刁。
解耦合
關(guān)鍵抽象
TestSuite
與TestResult
之間相互依賴芥牌,可以引入一個(gè)抽象的接口BareTestSuite
,兩者都依賴于一個(gè)抽象的BareTestSuite
聂使,使其兩者之間可以獨(dú)立變化壁拉,消除TestResult
對TestSuite
的反向依賴谬俄。
struct BareTestSuite {
virtual const Test& get() const = 0;
virtual void runBare(TestResult&) = 0;
virtual ~BareTestSuite() {}
};
私有繼承
TestSuite
私有繼承于BareTestSuite
,在調(diào)用TestSuite::run
時(shí)弃理,將*this
作為BareTestSuite
的實(shí)例傳遞給TestResult::runTestSuite
成員函數(shù)溃论。
struct TestSuite : Test, private BareTestSuite {
// ...
private:
void run(TestResult& result) override;
private:
const Test& get() const override;
void runBare(TestResult& result) override;
private:
std::vector<Test*> tests;
};
void TestSuite::runBare(TestResult& result) {
foreach([&result](Test* test) {
test->run(result);
});
}
const Test& TestSuite::get() const {
return *this;
}
// !!! TestSuite as bastard of BareTestSuite.
void TestSuite::run(TestResult& result) {
result.runTestSuite(*this);
}
通過私有繼承,TestSuite
作為BareTestSuite
的私生子痘昌,傳遞給TestResult::runTestSuite
成員函數(shù)钥勋,而TestResult::runTestSuite
使用抽象的BareTestSuite
接口,滿足李氏替換辆苔,接口隔離算灸,倒置依賴的基本原則,實(shí)現(xiàn)與TestSuite
的解耦驻啤。
反向回調(diào)
重構(gòu)TestResult::runTestSuite
的參數(shù)類型菲驴,使其依賴于抽象的、更加穩(wěn)定的BareTestSuite
骑冗,而非具體的谢翎、相對不穩(wěn)定的TestSuite
。
struct TestResult {
// ...
void runTestSuite(BareTestSuite&);
private:
std::vector<TestListener*> listeners;
};
#define BOARDCAST(action) \
for (auto listener : listeners) listener->action
void TestResult::runTestSuite(BareTestSuite& test) {
BOARDCAST(startTestSuite(test.get()));
test.runBare(*this);
BOARDCAST(endTestSuite(test.get()));
}
而在實(shí)現(xiàn)TestResult::runTestSuite
中沐旨,通過調(diào)用BareTestSuite::runBare
,將在運(yùn)行時(shí)反向回調(diào)TestSuite::runBare
榨婆,實(shí)現(xiàn)多態(tài)調(diào)用磁携。關(guān)鍵在于,反向回調(diào)的目的地良风,TestResult
是無法感知的谊迄,這個(gè)效果便是我們苦苦追求的解耦合。
另外烟央,此處使用宏函數(shù)替換上述的模板函數(shù)统诺,不僅消除了模板函數(shù)的復(fù)雜度,而且提高了表達(dá)力疑俭。教條式地摒棄所有宏函數(shù)粮呢,顯然是不理智的。關(guān)鍵在于钞艇,面臨實(shí)際問題時(shí)啄寡,思考方案是否足夠簡單,是否足夠安全哩照,需要綜合權(quán)衡和慎重選擇挺物。
其持之有故,其言之成理飘弧;適當(dāng)打破陳規(guī)识藤,不為一件好事砚著。所謂“守破離”,軟件設(shè)計(jì)本質(zhì)是一門藝術(shù)痴昧,而非科學(xué)稽穆。
重構(gòu)分析
經(jīng)過重構(gòu),既有的TestSuite::run
職責(zé)搬遷至TestResult::runTestSuite
剪个。一方面秧骑,TestResult
暴露給TestSuite
接口由2
減少至1
,緩解了TestSuite
對TestResult
的依賴關(guān)系扣囊。另一方面乎折, 私有化了TestResult::startTestSuite
, TestResult::endTestSuite
成員函數(shù),使得TestResult
取得了更好的封裝特性侵歇。通過重構(gòu)骂澄,職責(zé)分配達(dá)到較為合理的狀態(tài)了。
解耦的關(guān)鍵在于抽象接口BareTestSuite
惕虑,在沒有破壞TestSuite
既有封裝特性的前提下,此時(shí)TestResult
完全沒有感知TestSuite, TestCase
存在的能力坟冲,所以解除了TestResult
對TestSuite, TestCase
的反向依賴。
相反溃蔫,TestSuite, TestCase
則依賴于TestResult
的健提。其一,單向依賴的復(fù)雜度是可以被控制的伟叛;其二私痹,TestResult
作為Test::run
的聚集參數(shù),它充當(dāng)了整個(gè)xUnit
框架的大動(dòng)脈和神經(jīng)中樞统刮。
按照正交設(shè)計(jì)的理論紊遵,通過抽象的BareTestSuite
解除了TestResult
對TestSuite
的反向依賴關(guān)系,使得TestResult
依賴于更加穩(wěn)定的抽象侥蒙,縮小了所依賴的范圍暗膜。