在xUnit实现模式中,存在TestCase, TestSuite, TestResult, TestListener, TestMethod等重要领域对象。
重构之前
在我实现Cut的最初的版本中,TestSuite与TestResult之间的关系是紧耦合的,并且它们的职责分配也不合理。
Cut是一个使用Modern C++实现的xUnit框架。
TestSuite: 持有Test实例集的仓库
TestSuite是一个持有Test实例列表的仓库,它持有std::vector<Test*>类型的实例集。它实现了Test接口,并覆写了run虚函数。此外,在实现run时,提取了一个私有函数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维护了Test实例的生命周期,初始时为空,并通过add接口添加Test类型的动态实例;最后,通过析构函数回收所有的Test实例。
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: 测试结果的收集器
TestResult的职责非常简单,作为Test的聚集参数,用于搜集测试结果。它持有TestListener实例集,当测试执行至关键阶段,将测试的状态和事件通知给TestListener。TestListener监听TestResult的状态变化,通过定制和扩展实现测试数据统计、测试进度上报、测试报表生成等特性。
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维护了TestListener实例集的生命周期。初始时该集合空,通过add接口添加TestListener类型的动态实例;最后,通过析构函数回收所有的TestListener实例。
另外,TestResult为TestSuite公开了两个事件处理接口startTestSuite, endTestSuite。需要特别注意的是,私有的函数模板boardcast并没有在头文件中实现,它在实现文件中内联实现,其消除了重复的迭代逻辑。
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);
});
}
职责分布不合理
如下图所示,TestSuite::run方法依赖于TestResult的两个公开成员函数startTestSuite, endTestSuite。
重构之前
观察TestSuite::run的实现逻辑,其与TestResult关系更加紧密。因为,TestSuite::run调用TestSuite::runBare前后两个语句分别调用了TestResult的两个成员函数TestResult::startTestSuite, TestResult::endTestSuite完成的。与之相反,TestSuite::runBare则与TestSuite更加紧密,因为它需要遍历私有数据成员tests。
据此推论,TestSuite::run的实现逻辑与TestResult关系更加密切,应该将相应的代码搬迁至TestResult。难点就在于,runBare在中间,而且又与TestSuite更为亲密,这给重构带来了挑战。
搬迁职责
重构TestResult
既然TestSuite::run的实现逻辑相对于TestResult更加紧密,应该将其搬迁至TestResult。经过重构,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);
}
重构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);
}
// ...
经过一轮重构,TestSuite虽然仅仅依赖于TestResult::runTestSuite一个公开接口,但TestResult也反向依赖于TestSuite::runBare,依赖关系反而变成双向依赖,两者之间的耦合关系更加紧密了。
但本轮重构是具有意义的,经过重构使得TestSuite与TestResult的职责分布更加合理,唯一存在的问题就是两者之间依然保持紧耦合的坏味道。
解耦合
关键抽象
TestSuite与TestResult之间相互依赖,可以引入一个抽象的接口BareTestSuite,两者都依赖于一个抽象的BareTestSuite,使其两者之间可以独立变化,消除TestResult对TestSuite的反向依赖。
struct BareTestSuite {
virtual const Test& get() const = 0;
virtual void runBare(TestResult&) = 0;
virtual ~BareTestSuite() {}
};
私有继承
TestSuite私有继承于BareTestSuite,在调用TestSuite::run时,将*this作为BareTestSuite的实例传递给TestResult::runTestSuite成员函数。
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成员函数,而TestResult::runTestSuite使用抽象的BareTestSuite接口,满足李氏替换,接口隔离,倒置依赖的基本原则,实现与TestSuite的解耦。
反向回调
重构TestResult::runTestSuite的参数类型,使其依赖于抽象的、更加稳定的BareTestSuite,而非具体的、相对不稳定的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()));
}
而在实现TestResult::runTestSuite中,通过调用BareTestSuite::runBare,将在运行时反向回调TestSuite::runBare,实现多态调用。关键在于,反向回调的目的地,TestResult是无法感知的,这个效果便是我们苦苦追求的解耦合。
另外,此处使用宏函数替换上述的模板函数,不仅消除了模板函数的复杂度,而且提高了表达力。教条式地摒弃所有宏函数,显然是不理智的。关键在于,面临实际问题时,思考方案是否足够简单,是否足够安全,需要综合权衡和慎重选择。
其持之有故,其言之成理;适当打破陈规,不为一件好事。所谓“守破离”,软件设计本质是一门艺术,而非科学。
重构分析
经过重构,既有的TestSuite::run职责搬迁至TestResult::runTestSuite。一方面,TestResult暴露给TestSuite接口由2减少至1,缓解了TestSuite对TestResult的依赖关系。另一方面, 私有化了TestResult::startTestSuite, TestResult::endTestSuite成员函数,使得TestResult取得了更好的封装特性。通过重构,职责分配达到较为合理的状态了。
重构之后
解耦的关键在于抽象接口BareTestSuite,在没有破坏TestSuite既有封装特性的前提下,此时TestResult完全没有感知TestSuite, TestCase存在的能力,所以解除了TestResult对TestSuite, TestCase的反向依赖。
相反,TestSuite, TestCase则依赖于TestResult的。其一,单向依赖的复杂度是可以被控制的;其二,TestResult作为Test::run的聚集参数,它充当了整个xUnit框架的大动脉和神经中枢。
按照正交设计的理论,通过抽象的BareTestSuite解除了TestResult对TestSuite的反向依赖关系,使得TestResult依赖于更加稳定的抽象,缩小了所依赖的范围。
正交设计:关键抽象








网友评论