Best Practices for Developing Maintainable and Debuggable Software Programs

Thread Starter

Kittu20

Joined Oct 12, 2022
434
Hey everyone,

I'm looking to gather some insights on how to write code that's not only functional but also easy to maintain, control versions effectively, and debug efficiently. I'd really appreciate it if you could share your experiences and best practices in these areas.

  1. Maintainability: What strategies or coding practices do you follow to ensure that your software remains maintainable over time? How do you balance between adding new features ?
  2. Version Control: What version control system do you prefer using for your projects? How do you manage different branches, handle conflicts, and ensure a smooth collaborative development process?
  3. Debugging: Could you share strategies you've found particularly helpful in developing debuggable software program?

Looking forward to learning from your experiences and insights! Thanks in advance for sharing.
 

Ya’akov

Joined Jan 27, 2019
8,505
Maintainability

Documentation
  1. Thorough commenting, including of functions (see below) but consisting of answers to the question "why?" not "what?". The code tells you what but the answer to why is often obscure, and that's why you need comments.

  2. Comments on functions specifying what they require and accept in terms of parameters; and what they return. Think of this as an internal API. In this way you should also consider it the "rules" (see: modularity below).

Modularity
  1. When designing your program, use functions for anything that is repeated more than twice. Do not cut and paste code blocks. Your main program should be mostly a list of function calls that shows the program flow with the meat of the code residing in reusable functions.

  2. Before you write any code, determine the names of all global variables and the functions you can work out that are needed. (these things can be added to as you discover new needs, but the heart of the code that you write first should be on this basis). This may not seem very important but names are like handles that let you grab the right things during program execution. When this is done right things that need to be done are very easy to do because you've already figured out the rôle of the variable or function in the program.
  3. Make sure the names reflect the function of the named thing as closely as you can. If there are real world analogues to things, use those names because they will contain the concepts that thing embodies and so the name will actually show you things about the named item that you didn't see when you named it. When this happens it is very satisfying and serves to confirm you chose the right name.

  4. When naming variables be consistent both conceptually and orthographically. Though longer names are harder to type repeatedly, a good IDE will autocomplete for you. If names have multiple elements, be sure to order them in such a way that the resulting sort order is useful, e.g.: serialReadCounter, and serialIncrementCounter rather than readSerialCounter, and incrementSerialCounter since sorting by scope is much more useful than sorting by action.

    It doesn't matter what orthographic convention you use, it matters that you use it consistently. Don't switch around.

  5. Before you write any code in functions, write an API for that function. In this case the idea of the API is a little different from the strict definition but shares the concepts. Work out, and document in place using comments:

    1. What the function requires and accepts in terms of passed parameters when called, and the name of the function's local variables that will contain each one. Additionally, any global variables that will be used.

    2. What the function returns with the same information.

    3. A brief explanation of what the function does and why it exists.
  6. Normalize all variables and functions. This is in the sense of database normalization. It entails the following ideas:

    1. Eliminate redundancy. Do not repeat the contents of a variable or function in another variable or function. (This does not include localizing globals inside functions which is a special case). If there is something the must be stored or done that has several consumers, do it once and use that instance for every case.¹

    2. Do only one thing with one container. That is, don't combine things in either variable or functions simply because from one point of view they are related. In database terms, it is like having fields for First Name and Last Name rather than just Name so that each element is individually available.

      In your code, make sure your variables do not contain more than one conceptual value. Scalars are generally simpler than things like arrays, but the name example above shows how scalars are also affected by this.

      For functions, do not include abilities that aren't inherently tied to each other. Don't mistake a single call's point of view with the global reality of the function. In some cases, you might choose to have a single function and select various parts by passed parameters, but be careful with this, it tends to become a cesspit of code and reduces the various advantages of modularity.

      Try to keep these things as clean as possible and the end result will be the ability to transparently replace the implementation of a particular function without making changes anywhere else. This is a huge maintainability win, but it depends on the ideas in "use OO", below.
  7. Use Object Oriented techniques (even if you are not using an OO language, or even objects!) The things that OO buys you are exceptionally important but they don't require using objects. If your language offers objects, learn and use them! If you don't use an OO language, you can substitute some discipline in your coding to get these advantages.

    1. Use encapsulation to protect the modularity of your functions. Never "reach into" a function and use code or change values, always use the , methods, getters, and setters you provide in your function. In a language that supports encapsulation, use it. In a non-OO language, strictly adhere to the API you documented above.

    2. Write your code with methods, getters, and setters even if it is procedural. In the procedural case, your methods are often just different functions, but sometimes, if the methods are tightly coupled, and act on the same passed or global data, or don't need data at all, they can be called as parameters passed to the function. Methods are a very powerful idea and you will thank yourself when you come back to your code in six months and need to update it.

      Getters and setters are ways to change the values of variables "owned" by a particular function. They should be the only way to make these changes. Again, this allows for the replacement of functions without side effects. If you make changes to variables in several places, you may have a nasty knot to untangle when a change is needed.
I am going to stop here, but this topic alone is book-worthy. All three of your questions are more than one book to properly answer. I hope what I did write is useful—incomplete and abruptly terminated though it is. Sorry about that. many things to do here.


1. Except if there is a good reason to do otherwise, as usual. There will be exceptions to every rule, part of the art of programming, or designing anything, is to be able to spot those cases and accommodate them. One heuristic for this is to see which thing is ultimately more difficult and do the easier one.
 

Thread Starter

Kittu20

Joined Oct 12, 2022
434
Maintainability

Documentation
  1. Thorough commenting (see below) but consisting of answers to the question "why?" not "what?". The code tells you what but the answer to why is often obscure, and that's why you need comments.
I have written the sample code with comments as you suggested. Please take a look at the program and Please let me know your feedback on comments.
C:
// Define constants with descriptive names for better readability

#define INPUT_PIN                          1
#define OUTPUT_PIN                         0
#define SENSOR                             PORTBbits.RB0
#define ACTUATOR                           LATBbits.LATB1
#define SENSOR_HIGH                        1
#define TRUE                               1
void main(void)
{
    // Configure RB0 as an input pin for a sensor (e.g., light sensor)
    // This enables the microcontroller to receive sensor data
    SENSOR = INPUT_PIN;  // Set RB0 as an input to read data from a connected sensor
   
    // Configure RB1 as an output pin to control an actuator (e.g., motor)
    // This allows the microcontroller to drive the actuator's behavior based on its logic
    ACTUATOR = OUTPUT_PIN;
   
    while (TRUE) {
       
        // Perform logic based on the sensor value to control the actuator
        // Read the value of the sensor connected to RB0
        if (SENSOR == SENSOR_HIGH) {
            // If sensor value is above to the threshold, turn on the actuator (RB1 high)
            ACTUATOR = ACTUATOR_ON;  // Set RB1 high to activate the connected actuator
        } else {
            // If sensor value is below the threshold, turn off the actuator (RB1 low)
            ACTUATOR = ACTUATOR_OFF;  // Set RB1 low to deactivate the connected actuator
        }
    }
}
Note that this is the temporary code to show the comments, it would not compile on the compiler because it is incomplete code.
 

Ya’akov

Joined Jan 27, 2019
8,505
// Perform logic based on the sensor value to control the actuator // Read the value of the sensor connected to RB0
These are what comments, they are just noise because the code already tells you.

You should only comment lines of code that don’t have a clear and apparent reason. In other words, if you debug a program and find you have to perform a workaround, say, setting a bit on a peripheral twice, that would be a candidate:

//GX777U resets the dont_explode register the first time it is set. Set twice to prevent an explosion.
 

Thread Starter

Kittu20

Joined Oct 12, 2022
434
These are what comments, they are just noise because the code already tells you.

You should only comment lines of code that don’t have a clear and apparent reason.
In this version of the code, I've added comments only where they provide additional clarity or context beyond what's already apparent from the code itself.
C:
// Define constants with descriptive names for better readability

#define INPUT_PIN                          1
#define OUTPUT_PIN                         0
#define SENSOR                             PORTBbits.RB0
#define ACTUATOR                           LATBbits.LATB1
#define SENSOR_HIGH                        1
#define TRUE                               1
void main(void)
{
    // Configure RB0 as an input pin for a sensor (e.g., light sensor)
    SENSOR = INPUT_PIN;
   
    // Configure RB1 as an output pin to control an actuator (e.g., motor)

    ACTUATOR = OUTPUT_PIN;
   
    while (TRUE) {
       
        // check sensor value is HIGH or LOW
        if (SENSOR == SENSOR_HIGH) {
            // If sensor value is above to the threshold, turn on the actuator (RB1 high)
            ACTUATOR = ACTUATOR_ON;
        } else {
            // If sensor value is below the threshold, turn off the actuator (RB1 low)
            ACTUATOR = ACTUATOR_OFF;
        }
    }
}
 
Last edited:

Thread Starter

Kittu20

Joined Oct 12, 2022
434
What is the version control in embedded projects, and how does it facilitate the management of changes, whether they involve transitioning between different controller sizes (e.g., 16-bit to 32-bit), switching microcontroller platforms (e.g., from PIC to ARM), or adapting to component availability issues (e.g., replacing a DS1307 RTC with a similar RTC)? Please provide insights into What is the version control in embedded projects how version control supports these scenario
 
Top