美文网首页Clean C++
Clean C++:使用私有继承解耦合

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

作者: 刘光聪 | 来源:发表于2019-06-04 17:31 被阅读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依赖于更加稳定的抽象,缩小了所依赖的范围。

    正交设计:关键抽象

    相关文章

      网友评论

        本文标题:Clean C++:使用私有继承解耦合

        本文链接:https://www.haomeiwen.com/subject/bjerxctx.html