The-Software-Experts




Google
 




   catch your bugs!
  Home
  Newsletter
  Forum
  Shop

  SW-Training
  - Software Design
  - Safe Coding in C
  - Software Inspection
  - Software Testing
  

  SW-Design
  - Architecture
  - Module Design
  

  Coding in C
  - Safe Coding in C
  

  Refactoring
  - Principles
  - Methods
  

  SW-Testing
  - Principles
  - Static Analysis
  - Inspections
  - Module Tests
  - Functional Tests
  - Integration Tests
  - Test Documentation
  - Links
  

  SW-Documentation
  - SGML Principles
  - Printing SGML
  - Links
  

  SW-Processes
  - Process Descriptions
  - Process Assessments
  - Self Evaluation
  - Food for Thought
  - Links
  

Safety Software
Design Training

IEC 61508 compliant
Safety Software
Design for Microcontroller

SW Document
Templates

CMMI and SPICE
compliant document
templates

SW Process
Description

CMMI Level 4 and
SPICE compliant SW
development procedure

SGML Package for
Documentation

Edit and print SGML
Documemts. Professional.
fast, easy to use.

Test Bench
for C/C++

Perl based test
environment for easy
component testing

Principles of Code Refactoring

Refactoring grew in an environment of big company information systems which are programmed in Java or C++. These systems need a good architecture to be able to integrate the sometimes millons of code lines programmed by various individuals or teams. However the features of the object oriented programming languages tend to substitute more detailed design rules. They are often regarded as being implicit in this environment. Therefore if you read e.g. Martin Fowlers book on refactoring and also some of the other literature you will miss clear rules about how the refactored code should look like. No doubt the recommended methods of refactoring are descibed, but it is left to experience and common sense of the programmer to apply the appropriate method to the given code. Having worked in a similar environment for some time I came across situations where a programmer would do a certain refactoring which another one would not have done the same way. It is left to personal taste and experience how the refactoring is done.

In the setting of software development for safety critical microcontroller applications things are different. First of all there is the MISRA guideline which aims at defining a sub-set of C for safe programming. The code which is produced has to comply with this guideline. On top of it it is common practice in this community to have a rigid set of low level design rules and design pattern which is usually company internal but each of the companies which is active in this field has something similar. The aim of refactoring therefore has to be first of all to bring the code into compliance with these rules and guidelines. On top of it some of the methods of the wider refactoring community, as e.g. described in Martin Fowler's book may be helpful to be applied to the code. This is the background of the refactoring principles I want to describe on these pages.

 

Refactor Software to comply with Design Standards

Refactoring should have clear goals. It is no good to just poke around in a code and see what you can do to make it better. There should be e.g. clear design guidlines which a code has to comply to and coding pattern which a code has to use. If you do not have such guidelines and pattern for microcontroller programming you should set them up as soon as possible.

  1. Use the MISRA rules as a definition of a safer subset of the C syntax. This will eliminate the pitfalls and traps you can run into when you are programming in C. There are automatic code checkers which check your code for MISRA compliance. It is recommendable to use one of them and employ it in your refactoring. Since MISRA does not publish their guideline, but rather chooses to sell it to each company which wants to apply it, you may want to fall back to a cheaper solution. Get hold of a copy of Andrew Koenig's book "C pitfalls and traps" which costs only a few Dollars and covers most of what MISRA has to say. At least it covers the really important items and skips the nonsense.
  2. Define templates and principles of design as e.g. the object oriented approach which I described in the design pages of this web-site. These principles prohibit global variables and promote the encapsulation of data in an object. The access to the data of such an object will be done via get functions or set functions like it is common to C++ or Java OO programming. Here we are enforcing something by refactoring activities which is no headache at all for the wider community, because the use of Java or C++ automatically implies this way of design.
  3. Define additional rules and quality metrics for your low level design. E.g. the number of get functions for an object is ideal if it is smaller or equal to 10. Up to 20 may be permissible. More than 20 is an indicator that your object is too big and you should spit it up. Another rule which is valueable is that a C-function should not be bigger than what you can display on one screen page. Split up any bigger ones.
  4. Last but not least there are a few things which are not covered by MISRA or the rules of a good OO design. E.g. the use of bitfields is allowed in MISRA, although there are some restrictions. OO design is also not in contradiction with bitfields. However I found out that the use of bitfields makes your code not portable and generates pitfalls e.g. when used in calculations. Therefore I prohibited the use of bitfields in my designs and provide some smart code pattern to substitute them. This pattern is for additional safety and portability. There may be other pattern which provide a faster execution of the code and thus save runtime on the microcontroller compared to just doing normal programming. They do not make things too different but when they are applied the resource issue is under control right from the beginning. These were just a few examples of pattern which you have to set up for your environment. Refactoring should make sure that they are consistenly used in the code.

You should try to improve the code to comply to these rules and standards, and stop refactoring when the goal is reached. Of course in the process of doing this, there will be enough places to do things smarter, better and faster.

 

Some more "Soft Goals"

Other goals may be to achieve easier maintenance and improved understandability e.g. by adding comments or regrouping functions into other modules. You can split up functions or extract portions of a function to build a new sub-function. On the other end of the scale you can combine functions, to make one out of two. This may also involve the modification and improvement of interfaces, data structures and ways of programming an algorithm. Most of these items can not be easily described in a design guideline. Therefore I called them "Soft Goals". It all depends on personal experience and sometimes taste of the programmer. A certain amount of freedom is granted here for own ways of improving the code, however it never should violate clear design rules and resource constraints.

 

No Functional Changes by Refactoring

Code refactoring has an improved design and code as its output and never any functional changes. The functionality of the improved code is the same as it was for the old code. Changes of the functionality in the progress of refactoring is strongly discouraged, even if it was found to be faulty. Finish the refactoring, make sure by sufficient tests that you did not add new functionality, that you did not reduce or modify functionality, and then work again on the functionality.

 

Set up sufficient Tests prior to Refactoring

Prior to refactoring the functional regression tests (repeatable tests) have to be set up. You should use a suitable test environment for this. These tests will be reused to check the functionality of the improved code.The focus should be that the development loop from changing the code to compiling it and finally running the tests should be easy to handle or even be automatic. Further the loop should be fast in execution and the comparison of the latest results produced by the refactored code with the initial results should be also automatic.

In the refactoring I performed for microcontroller software in many cases we had the lucky situation that a simulation environment was existing. The environment could read test data from a file, it executed the code to be simulated or tested, and it produced an ASCII text file which contained a log of many internal variables of the code for each execution cycle of the tested software. Thus I could produce an initial set of these log files and compared the log files of the refactored code with the original files. For the compare I used a file difference tool which could be set up in a way that only different file portions were displayed. With this environment it was only a matter of a few seconds to compile the improved code, generate the DLL and load it to the simulation tool. There it only required the push of a button to run the test cases and produce the new output files. The file differencer was always open, reloaded the new files automatically and if there was no display everything was fine.

For other refactorings I used the Perl testing environment which is described on this web-site in the testing section. It took something like an hour to set up the test scripts in this environment. The source code was part of the same text file as the Perl tests. The code could be refactored and retested as easy as in the simulation environment. After refactoring was finished it could be taken out of the environment and placed into the original files.

The secret for success is that you should have test cases which cover all the necessary values and ranges to detect immediately if refactoring led to an error. To achieve this it can be helpful to check the code coverage of the test cases with an appropriate tool. The higher the coverage the more likely your test cases are sufficient. Better would be to apply the classical methods of testing and check that all equivalence partitions were covered and that there are sufficient test cases to cover all boundaries (see the paegs on testing on this web-site). Considering all these items it is at hand that the best way is to set up the tests in parallel to the development phase and simply re-use them for refactoring purposes. A last hint is that you should perform always small refactoring steps and then re-execute the test. This way you immediately detect if you made an error and are likely to find it very fast. You are in big trouble if you perform a lot of changes in the process of refactoring and then are stuck with a code which does not work any more.

 

Apply Refactoring prior to performing formal Tests

You can not test quality into software. You have to design it into software. Although refactoring can not substitute a good architecture and design, it should be applied as an additional highly effective method to improve software quality. Refactoring should be applied prior to any formal software tests. Formal tests have the rule that the tester must not change code. He only executes the tests and reports the faults. I experienced great success to have some refactoring phases prior to executing formal tests. These refactoring activities were done in cooperation of the developer and the tester. It could be regarded as an informal test where the tester could bring in all his knowledge, modify and improve the code also from his point of view, without having the overhead of writing test reports etc. It helped to fix bugs, improve the structure and performance, and improve the maintainability and understandability of a code. We applied it in several iterations and eventually later on it made the tests a lot easier. Testing literature recommends informal software tests prior to a formal test to reduce the number of heavy formal test iterations. Refactoring, applied in a systematic and defined way, constitutes such an informal software test in combination with an instant bug fix and code improvement. The involvement of a tester during the refactoring also guarantees that the necessary regression tests which are needed in the process of refactoring are of better quality, since the tester usually has the theoretical background to achieve a good test coverage (boundary checks, equivalence classes, etc.).

 

Architecture and Refactoring

Since refactoring is mostly concerned with individual portions (modules or units) of a complete software, it may not be able to improve the overall architecture of the software. You have to be aware of this! Refactoring comes in at a certain level of your desing. This is the module design level. Thus it can not improve the high level design (or better called architecture). If you missed to introduce a needed layer in your architecture, or if you scattered a certain functionality over several modules instead of providing a dedicated functional block, you are very unlikely to patch the problem by refactoring. You better should go first of all for a redesign of the architecture. It takes a real expert to detect this during the refactoring, stop refactoring and go for the architectural redesign, instead of turning out the programmer's pride to try and fix by a mason's job what would be an architect's job.

 

Resource Optimizations and Porting Software

In the microcontroller world you sooner or later come across the need to save resources. This could be done in one of the refactoring iterations I propagated. However I frequently observed that a rework of the code to save memory or runtime results in the same time in a loss of overall quality and in a loss of the other refactoring goals. You should employ refactoring if you have certain desing pattern which are made to support resource optimization and apply these pattern to all code portions where this is possible. This can and should be done in a refactoring. The other way of saving resources is to reduce code lines. Less code lines means less ROM, RAM and time to execute. This can be also done by refactoring which is always a friend of making things easier, smaller and cleaner. However you should refrain from over-optimizing the code. There are ways of messing up a code which make it a bit faster and smaller, but not maintainable and understandable any more. This contradicts refactoring completely. You should avoid this. Before you plunge into an adventure like this you should re-think the overall system again. You should see if this is the only way to save resources. In all cases I came across so far the real problems were at other places in the system which could be solved more easily instead of messing up the complete code.

This goes hand in hand with making a code portable to other platforms. It is possible to make a code almost completely portable across all CPU platforms. The only exception is the hardware abstraction layer which needs to be adapted to each HW platform. The rest of the code can and should be programmed independently of any platform. There are pattern which enable this. The pattern consist mainly of avoiding certain ways of coding. Refactoring can be applied to use these pattern in the code. This will not squeeze the last nano second out of your CPU runtime and it also will need a bit more ROM and RAM, but it will keep the code understandable and maintainable. Therefore the goal of portability and refactoring are to a large degree overlapping.









Imprint