Clean C++:使用私有繼承解耦合

xUnit實(shí)現(xiàn)模式中,存在TestCase, TestSuite, TestResult, TestListener, TestMethod等重要領(lǐng)域?qū)ο蟆?/p>

重構(gòu)之前

在我實(shí)現(xiàn)Cut的最初的版本中填帽,TestSuiteTestResult之間的關(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í)例仲翎。

另外痹扇,TestResultTestSuite公開了兩個(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

重構(gòu)之前

觀察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)使得TestSuiteTestResult的職責(zé)分布更加合理,唯一存在的問題就是兩者之間依然保持緊耦合的壞味道姐刁。

解耦合

關(guān)鍵抽象

TestSuiteTestResult之間相互依賴芥牌,可以引入一個(gè)抽象的接口BareTestSuite,兩者都依賴于一個(gè)抽象的BareTestSuite聂使,使其兩者之間可以獨(dú)立變化壁拉,消除TestResultTestSuite的反向依賴谬俄。

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,緩解了TestSuiteTestResult的依賴關(guān)系扣囊。另一方面乎折, 私有化了TestResult::startTestSuite, TestResult::endTestSuite成員函數(shù),使得TestResult取得了更好的封裝特性侵歇。通過重構(gòu)骂澄,職責(zé)分配達(dá)到較為合理的狀態(tài)了。

重構(gòu)之后

解耦的關(guān)鍵在于抽象接口BareTestSuite惕虑,在沒有破壞TestSuite既有封裝特性的前提下,此時(shí)TestResult完全沒有感知TestSuite, TestCase存在的能力坟冲,所以解除了TestResultTestSuite, TestCase的反向依賴。

相反溃蔫,TestSuite, TestCase則依賴于TestResult的健提。其一,單向依賴的復(fù)雜度是可以被控制的伟叛;其二私痹,TestResult作為Test::run的聚集參數(shù),它充當(dāng)了整個(gè)xUnit框架的大動(dòng)脈和神經(jīng)中樞统刮。

按照正交設(shè)計(jì)的理論紊遵,通過抽象的BareTestSuite解除了TestResultTestSuite的反向依賴關(guān)系,使得TestResult依賴于更加穩(wěn)定的抽象侥蒙,縮小了所依賴的范圍暗膜。

正交設(shè)計(jì):關(guān)鍵抽象
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市鞭衩,隨后出現(xiàn)的幾起案子学搜,更是在濱河造成了極大的恐慌,老刑警劉巖醋旦,帶你破解...
    沈念sama閱讀 218,546評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件恒水,死亡現(xiàn)場離奇詭異,居然都是意外死亡饲齐,警方通過查閱死者的電腦和手機(jī)钉凌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來捂人,“玉大人御雕,你說我怎么就攤上這事矢沿。” “怎么了酸纲?”我有些...
    開封第一講書人閱讀 164,911評論 0 354
  • 文/不壞的土叔 我叫張陵捣鲸,是天一觀的道長。 經(jīng)常有香客問我闽坡,道長栽惶,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,737評論 1 294
  • 正文 為了忘掉前任疾嗅,我火速辦了婚禮外厂,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘代承。我一直安慰自己汁蝶,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,753評論 6 392
  • 文/花漫 我一把揭開白布论悴。 她就那樣靜靜地躺著掖棉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪膀估。 梳的紋絲不亂的頭發(fā)上幔亥,一...
    開封第一講書人閱讀 51,598評論 1 305
  • 那天,我揣著相機(jī)與錄音察纯,去河邊找鬼紫谷。 笑死,一個(gè)胖子當(dāng)著我的面吹牛捐寥,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播祖驱,決...
    沈念sama閱讀 40,338評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼握恳,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了捺僻?” 一聲冷哼從身側(cè)響起乡洼,我...
    開封第一講書人閱讀 39,249評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎匕坯,沒想到半個(gè)月后束昵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,696評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡葛峻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,888評論 3 336
  • 正文 我和宋清朗相戀三年锹雏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片术奖。...
    茶點(diǎn)故事閱讀 40,013評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡礁遵,死狀恐怖轻绞,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情佣耐,我是刑警寧澤政勃,帶...
    沈念sama閱讀 35,731評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站兼砖,受9級(jí)特大地震影響奸远,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜讽挟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,348評論 3 330
  • 文/蒙蒙 一懒叛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧戏挡,春花似錦芍瑞、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至妥凳,卻和暖如春竟贯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背逝钥。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評論 1 270
  • 我被黑心中介騙來泰國打工屑那, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人艘款。 一個(gè)月前我還...
    沈念sama閱讀 48,203評論 3 370
  • 正文 我出身青樓持际,卻偏偏與公主長得像,于是被迫代替她去往敵國和親哗咆。 傳聞我的和親對象是個(gè)殘疾皇子蜘欲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,960評論 2 355

推薦閱讀更多精彩內(nèi)容