Clearness and testic trade-off

xiaoxiao2021-03-06  15

Clearness and testic trade-off

Deng Hui

If you are the same as my hobby, then you will definitely spend a lot of time to think about such a question: "What is the thing makes a design can be called good?" Most software developers will career in their careers. At some moment, I have paid attention to this problem. At this moment, I often witnessed the negative effects of a bad design. At that moment, we started reflection. In this reflection, we will experience such a stage, we seem to feel that we know what is a good design, but it is unable to define it. We have learned a variety of design principles and experience laws. Through these principles and rules, we can easily determine a good design constituent element. But when these principles and rules have conflicts, we have to make trade-offs and determine the best choice for each different situation.

In recent years of software development, through our own practice, experience and refer to the large number of experiences of others, I also define a task force to help themselves can make effective judgment when weigh. This rule is: keeping design as clear as possible. I am very convinced that whenever we need to do it, clarity is the most important. If we can use a simple and clear coding style while we have developed, each method and class have an adult name, and it is very easy to understand, there is no confusion that is tangled together in the system. Code. Then this provides a good foundation for any work next step. You can calmly change the system without worrying about what damage, you can write tests for the system, do some adjustments, increase characteristics, etc. And all of this will become relatively easy due to the clarity of the system itself.

Therefore, "clear design is a good design" seems to be a very reasonable experience, after all, the reason for the unoperable reason is ultimately attributed to the lack of clarity. If a system can be understood well, then we will change it very effectively. Otherwise, it will become very difficult. This argument sounds very simple, isn't it? However, it is indeed too simple. In my recent reconstruction of the system, I found that I always sacrificed clarity from time to time and put its priority below another standard.

Change clear code

Let's take a look at an example. The following C code is a method in the PSTN protocol class in our current project, which looks a fairly clear code.

Void Pstn :: onhookoff_an1 (int8u *, int32s)

{

SetState (AN2);

Sendestablish :: Hookoff;

T1_.Start ();

}

This method describes how the processing logic when the PSTN protocol state machine is received in the AN1 state, and the HOOKOFF message is received. Anyone who has seen the PSTN protocol text should understand the meaning of this code expression very quickly, as it briefly describes the logic specified in the agreement text. It migrates the state to AN2 and sends an Establish message with a steady-state signal hookoff, and starts the timer T1. Where t1 is a timer object, it is an instance of the TimerWrapper class, which exists as a member variable of the PSTN class.

So, what should we do when we want to change the timer's implementation logic? The answer seems to be quite simple. We find the implementation of the timer class directly and change its logic. But how do we know what we do is correct?

Ensure that the changes we have done are the correct way to write some tests for the code we want to change. In this way, we can learn about its current working conditions. Since then, when we do changes, we can write more tests to check if the changes do it have a desired behavior.

Therefore, the first step we have to do is to create a PSTN instance in the test, and call its onhookoff_an1 method, then check if the T1 timer is started. The code snippet shows the test code written by using the CPPUnitLite (http://cppunit.sourceforge.net) test framework. Test (PSTN, Hookoff_an1)

{

L3ADDR Addr = 0;

PSTN PSTN (ADDR);

Pstn.setState (AN1);

Pstn.ookoff_an1 (0, 0);

Check (pstn.gett1 (). Isruning ());

}

It looks very simple, but when we run test, I have trouble. The TimerService class is used in the Start method of our timer class (this is a timer implementation class), and this class is also related to the specific timing hardware and the operating system platform. We want to use CPPUnitlite to test this class in the Windows platform, you need to bring relevant classes and do some annoying modifications. What should we do can test our small code without introducing other parts of the system?

Reconstructing code makes testability

This is like a job that is refactored to be completed. Reconstruction is such a process: Changing the structure of the code but does not change the behavior of the code, making it easier to maintain (more information, you can refer to the Martin Fowler Refactoring: Improving The Design of EXISTING CODE book). For this example, we want to reconstruct the PSTN class, making it easy to test, and make it easier to maintain. We have to remember that when we reconstruct it, there must be a test for the reconstructed code, so you can make sure we don't introduce an error.

In order to solve this problem, we need to use some "dependent elimination" techniques to make this class easy to test. Relying on eliminating technology is refactoring technology, but they are very conservative. In general, we can safely implement them without running testing. (For further information, please refer to Michael Feathers' Working Effectively With Legacy Code.)

So how do we make the code easier to test? Here is the technology we use. We first locate the Start and ISRunning methods in the TimerWrapper class, which declares them as a Virtual method (see the code snippet below). This way, we can define the subclasses of TimerWrapper, and Override's Start and ISRunning methods to provide the logic we need to test. And, as long as we make changes to the PSTN class, add a sett1 method to it. We can use the instance of the subclass in PSTN, eliminating the dependence of the hardware and operating system platforms in these two methods. After doing this, we can easily write other method tests in the PSTN class.

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 start;

}

Void Pstn :: onhookoff_an1 (int8u *, int32s)

{

SetState (AN2);

Sendestablish :: Hookoff;

T1_-> start ();

}

Void Pstn :: onhookoff_an1 (int8u *, int32s)

{

SetState (AN2);

Sendestablish :: Hookoff;

T1_-> start ();

}

Void Pstn :: onhookoff_an1 (int8u *, int32s)

{

SetState (AN2);

Sendestablish :: Hookoff;

T1_-> start ();

}

Test (PSTN, Hookoff_an1)

{

L3ADDR Addr = 0;

PSTN PSTN (ADDR);

FakeTimer1 T1;

Pstn.setState (AN1);

Pstn.Sett1 (& T1);

Pstn.ookoff_an1 (0, 0);

CHECK (T1.RUNING ());

}

The technology we used is a variant of Introduce Instance delegator technology. After we use this technology to overcome the difficulties we face, we can write tests for hookoff_an1 methods. This changed structure is more complicated before the change, and we have to find all places using TimerWrapper, change it to the form of settings to set the form to facilitate testing.

It can be seen that this is not the most simple solution. We have to inherit TimerWrapper and have to change those classes that can be directly used in the member variables TimerWrapper to a more indirect form. So, should we have some uncomfortable feelings because of the code complicated? We do have this feeling, but we are also moving on the road that makes the code more easily maintained. The fact is this: either we have to do this, or we must use some of the same (or even more) to test the code. The original code is really simple and clear, but it is not easy to test. For me, this is this to make it a bad design.

I really like neat, clear code. I think clarity is one of the most important principles we must guarantee when we design. However, when I started to try to conduct some tests on the code that looks very clear, I would rather lose some of the clarity (or even global) to reach the need for testability. When the code becomes test, we have obtained another type of clarity. We can make the code's functionality clearer by writing tests, and we can also reconstruct the code under the protection of these test code, making it gradually have a clearness of the traditional sense.

Yes, we can continue to reconstruct the PSTN class, which gradually approaches our clarity in the traditional sense, and we have written in this process ensures that we have not cause any other damage in this process. Similarly, we can use a similar approach to most designs, making them first easier to test. Once we do this, we can make it clearer in the next development.

转载请注明原文地址:https://www.9cbs.com/read-46636.html

New Post(0)