清晰性和可测试性的权衡

    技术2022-05-11  2

    清晰性和可测试性的权衡

                  保持设计尽可能的清晰

           更改清晰的代码

    C++PSTNvoid Pstn::OnHookOff_An1(INT8U*, INT32S)

    {

        SetState(AN2);

        SendEstablish(SteadySignalTypeCode::HOOKOFF);

        t1_.Start();   

    }

    PstnAN1HookOffPSTNAN2HOOKOFFEstablisht1t1TimerWrapperPstn

    PstnOnHookOff_An1t1CppUnitLitehttp://cppunit.sourceforge.net

    TEST(Pstn, HookOff_An1)

    {

        L3ADDR addr = 0;

        Pstn pstn(addr);

        pstn.SetState(AN1);

        pstn.OnHookOff_An1(0,0);

        CHECK(pstn.GetT1().IsRuning() );

    }

    StartIsRunningTimerServiceWindowsCppUnitLite

    对代码进行重构使之具有可测试性

    这听上去像是重构应该完成的工作。重构是这样一个过程:更改代码的结构但是不改变代码的行为,使之变得更加易于维护(更多信息,可以参考Martin FowlerRefactoring: Improving the Design of Existing Code一书)。针对本例来说,我们希望对Pstn类进行重构,使之易于测试,从而也使之更加易于维护。我们要记住,当我们进行重构时,必须要有针对要重构代码的测试存在,这样就可以确保我们不会引入错误。

    为了解决这个问题,我们需要使用一些依赖消除技术来使该类变得易于测试。依赖消除技术都是重构技术,但是它们非常的保守。一般来讲,无需运行测试,我们就可以安全地实施它们。(进一步的信息,请参考Michael FeathersWorking Effectively with Legacy Code一书。)

    那么我们如何把代码变得更加易于测试呢?下面是我们所采用的技术。我们首先找到TimerWrapper类中的StartIsRunning方法,把它们申明为virtual方法(请参见下面的代码片断)。这样,我们就可以定义TimerWrapper的子类,并override其中的StartIsRunning方法,以提供我们测试所需要的逻辑。并且,只要我们对Pstn类稍做更改,给它增加一个SetT1方法。我们就可以在Pstn中使用该子类的实例,从而消除了这两个方法中对硬件和操作系统平台的依赖。这样做了以后,我们还可以方便地编写Pstn类中其他使用定时器的方法测试。

    class TimerWrapper

    {

       ...

    public:

        // change these two methods to be virtual

        virtual void Start();

        virtual bool IsRunning();

        ...  

    };

     

     

     

    class FakeTimer1 : public TimerWrapper

    {

    public:

        FakeTimer1()

        {

           started = false;

        }

       

    public:

        void Start()

        {

           started = true;

        }

           

        bool IsRunning()

        {

           return started;

        }

       

    public:

        bool started;

    };

    void Pstn::OnHookOff_An1(INT8U*, INT32S)

    {

        SetState(AN2);

        SendEstablish(SteadySignalTypeCode::HOOKOFF);

        t1_->Start();   

    }

    void Pstn::OnHookOff_An1(INT8U*, INT32S)

    {

        SetState(AN2);

        SendEstablish(SteadySignalTypeCode::HOOKOFF);

        t1_->Start();   

    }

    void Pstn::OnHookOff_An1(INT8U*, INT32S)

    {

        SetState(AN2);

        SendEstablish(SteadySignalTypeCode::HOOKOFF);

        t1_->Start();   

    }

    TEST(Pstn, HookOff_An1)

    {

        L3ADDR addr = 0;

        Pstn pstn(addr);

    FakeTimer1 t1;

        pstn.SetState(AN1);

        pstn.SetT1(&t1);

        pstn.OnHookOff_An1(0,0);

        CHECK(t1.IsRuning() );

    }

     

     

     

    我们上面所采用的技术是Introduce Instance Delegator技术的一个变种。我们使用该技术克服了我们所面临的困难后,就可以为HookOff_An1编写测试了。这个更改后的结构比更改前要复杂一些,并且我们还不得不找到所有使用TimerWrapper的地方,把其更改为使用Set方式设置的形式,以便于我们可以比较容易地进行测试。

    可以看出,这并不是最为简洁的解决方案。我们得继承TimerWrapper并且还得把那些原来可以直接以成员变量方式TimerWrapper的类更改为更为间接的形式。那么,我们应不应该由于代码变得复杂而具有某些不太好的感觉呢?我们确实会有这种感觉,但是我们同样也在使得代码更加易于维护的道路上前进了一步。事实就是这样:要么我们就得这么做,要么我们就必须得使用一些同样(甚至更加)复杂得手段来对代码进行测试。原来的代码确实更加简单清晰,但是却不易测试。对我来说,正是这一点使它成为一个糟糕的设计。

    我非常喜欢整洁、清晰的代码。我认为清晰性是我们在设计时必须要保证的最为重要的原则之一。但是,当我开始在那些看上去很清晰的代码上试图去进行一些测试时,我就会考虑宁愿丧失掉一些清晰性(甚至是全局上的)来达成需要的可测试性。当代码变得可测试时,我们就获得了另外一种类型的清晰性。我们可以通过编写测试使得代码的功能更加清楚,并且我们还可以在这些测试代码的保护下,对代码进行重构,使之逐渐变得具有传统意义上的清晰性。

    是的,我们可以继续对Pstn类进行重构,使之逐渐接近我们在传统意义上的清晰性,并且我们在这个过程中所编写的测试可以确保我们在这个进程中没有造成任何其他的破坏。同样,我们可以针对大多数的设计采用类似的方法,使它们首先变得更加易于测试。一旦我们做到了这一点,我们就可以在接下来的开发中使之变得更加清晰。


    最新回复(0)