Clean C++

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

2019-06-04  本文已影响0人  刘光聪

xUnit实现模式中,存在TestCase, TestSuite, TestResult, TestListener, TestMethod等重要领域对象。

重构之前

在我实现Cut的最初的版本中,TestSuiteTestResult之间的关系是紧耦合的,并且它们的职责分配也不合理。

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实例集,当测试执行至关键阶段,将测试的状态和事件通知给TestListenerTestListener监听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实例。

另外,TestResultTestSuite公开了两个事件处理接口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,依赖关系反而变成双向依赖,两者之间的耦合关系更加紧密了。

但本轮重构是具有意义的,经过重构使得TestSuiteTestResult的职责分布更加合理,唯一存在的问题就是两者之间依然保持紧耦合的坏味道。

解耦合

关键抽象

TestSuiteTestResult之间相互依赖,可以引入一个抽象的接口BareTestSuite,两者都依赖于一个抽象的BareTestSuite,使其两者之间可以独立变化,消除TestResultTestSuite的反向依赖。

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,缓解了TestSuiteTestResult的依赖关系。另一方面, 私有化了TestResult::startTestSuite, TestResult::endTestSuite成员函数,使得TestResult取得了更好的封装特性。通过重构,职责分配达到较为合理的状态了。

重构之后

解耦的关键在于抽象接口BareTestSuite,在没有破坏TestSuite既有封装特性的前提下,此时TestResult完全没有感知TestSuite, TestCase存在的能力,所以解除了TestResultTestSuite, TestCase的反向依赖。

相反,TestSuite, TestCase则依赖于TestResult的。其一,单向依赖的复杂度是可以被控制的;其二,TestResult作为Test::run的聚集参数,它充当了整个xUnit框架的大动脉和神经中枢。

按照正交设计的理论,通过抽象的BareTestSuite解除了TestResultTestSuite的反向依赖关系,使得TestResult依赖于更加稳定的抽象,缩小了所依赖的范围。

正交设计:关键抽象
上一篇下一篇

猜你喜欢

热点阅读