Chapter 21: Writing High-Quality, Debuggable Code
At this point in the book, we have studied the C programming language and have had lots of practice writing code for Pebble smartwatches. In most chapters, we have outlined how to write good C code. While it is essentially impossible to write perfect code, we can write simple, understandable code with as little undocumented magic as possible. In this chapter, we will conclude this book with a look at how to write high-quality code that is easy to read and debug.
This chapter will contain an outline of tips and techniques that make C code readable. We will conclude the chapter with an examination of software tools that help with readable code, include integrated development environments and Pebble packages.
Standards and Definitions
We start this look at good coding by looking at standards. We want define "high-quality" and "debuggable" with C language standards.
First, let's look at the C compiler itself. The current compiler used to compile Pebble smartwatch apps is GCC version 4.7.2. This compiler supports many C language standards (see this link for a list), but we focus here on the fact that it supports standard "c11". This means "C standard from 2011".
Next, we should define what we mean by "high-quality" code and code that is "debuggable". Often, there's a "I know it when I see it" definition to what good code can be, but that's not very satisfying, or repeatable, when we want to write good code. Here are several properties to apply to a definition of code quality.
- Good code is readable code. Your code should be readable by yourself and others. Readable code is like a good book: you enjoy reading it and it just makes sense to you. It has an elegant property to it that makes you want to continue working with it.
- Good code is simple code. Simple code is concise, focused, and organized. It does a single thing well.
- Good code is efficient code. This means that your code is economical. It uses resources quickly and cleanly. Again, it does a single thing well. Efficiency takes design effort from the beginning of writing your code.
- Good code is maintainable code. This is often phrased in terms of other developers. Good code is code that can be maintained by developers other than the code's creator. Code maintenance can be defined as code that can be understood, explained, and extended by other developers.
- Good code is clear code. In a way, clear code is summation of the above properties. Programming faces the constant challenge of managing complexity. Maintainable code that others can understand and read well is clear code. Code that is simple and efficient is clear code.
So, clear, simple, readable code that is efficient and maintainable is our goal.
Of course, the computer in your smartwatch does not care if your code is good or bad quality code. High-quality standards are for humans.
Finally, note that the code that we write is likely a balance of the above traits. Sometimes, simple code is not as efficient as we like. So thinking of art of creating high-quality, debuggable code as a balancing act can help when your code does not exactly have every property.
Design and Organize Your Code First
We began this book discussing abstraction and how a programmer can exploit abstraction to make good, clear code. The first step towards high-quality code is to revisit abstraction. Designing and organizing your code to focus on the algorithm and events you are using is essential.
Before writing C code, take a few minutes to write out the steps to your algorithm and which functions will implement each step. Write out the parameters each function needs. Write out the global and local pieces of information (i.e., variables) each function will need. Finally, draw calling relationships between the functions. Do this before writing new functions into your program.
Note that Cloud Pebble will build a minimal framework for you if you request it. It's a good idea to start with this, because the framework for window initialization and events is then already in place. Begin with a diagram of this framework, including the global definitions.
The important thing here is that your program logic use functions without regard to their implementation details. If a function does too much, break it up. Each function can be written in terms of yet lower level functions. The goal is to arrive at subtasks that are simple to implement with relatively few statements. This approach is an excellent aid to program development; if the subtasks are simple enough, it is easier to produce bug-free reliable programs.
Clear, Obvious, Simple
In Chapter 3, we gave several ways to write good, clear code. These included:
- Use meaningful variable names and expressions, even when literals will work.
- Use names for boolean values rather than literals.
- Use assignment operators as statements only, not in expressions.
- Avoid using shortcuts in expressions.
- Limit all name access to the tightest possible block.
- Use casting to make sure types are converted.
These items all ensure that code is clear, obvious, and simple. It's tempting not to use obvious code. Often, it can be tedious to use code that seems too simple. In fact, it's even more fun to demonstrate coding prowess by writing algorithms that are a challenge to read. However, this standard test applies: can you easily understand your code 6 months from now, when the context and coding problem are no longer part of your thinking?
This also applies to functions. Make functions short and simple, implementing a algorithm step.
See this link for an xkcd spin on bad code: here.
Comment Comment Comment
Another obvious assist to good code can also be a bit tedious: commenting your code. Adding comments as you are coding is the best approach and the hardest to convince yourself to do. After all, why comment code that you obviously know how it works? Again, you must apply the 6-month test: can you read it clearly after 6 months away from the code's creation?
Coupling refers to the degree of association or dependence of one segment of code or a function with another. The goal in well-written code is have functions and code segments that are loosely coupled with one another.
We often describe coupling like this: if a function or code block were to change, how would that affect other parts of the code?
Here's an example. Let's say that a section of code has two variables and two functions defined inside it. If both functions were to rely on those two global variables in some way, we say that's a tight coupling. The outer block cannot change how those variables are manipulated without hurting the operation of the two inner functions. Now let's say that each function were to define two parameters and the caller must send the global variables as parameters. This is now a looser coupling, because each function only uses parameters, which the outer block has to send. In fact, the outer block can change those two variables any way it can, as long as it sends the function the right parameters.
Coupling is kept more loose when abstraction is higher, data is separated or data and interfaces are standardized. Using standard formats for images, like PNG formats, is a way to make coupling looser, because the formats are very likely to stay consistent. Writing separate "getter" functions for each piece of data makes for looser coupling than receiving data in a big chunk, because data can be retrieved in any order rather than forced into a specific order.
Use External Code
Sometimes organization needs modularization. To properly design your code into modules and organize those modules, place them in external files, linked together by prototype ".h" files. Prototype files list the prototypes of each function defined externally; they are all you need to reference the functions, since the system will merge your code modules together before creating the executable program.
Don't Repeat Yourself
If you use the same sequence of code several times over, that is, if you catch yourself cutting and pasting the same code several times, it makes more sense to write the code into function and call the function. It's been said that good programmers are lazy programmers because they don't want to repeat code over and over.
Using a function for repeated code not only saves repetition, but, if naming is done correctly, it helps to document the code.
Assert Conditions and Handle Errors
If you have a code design and you have code that is organized into loosely coupled functions or code blocks, then it's possible to establish pre- and post-conditions to your code. These are data values and other configurations that should exist before your code executes and after execution is completed.
For example, perhaps divisors should be non-zero or time values should not be negative.
When these types of conditions exist, your code should check those conditions and act on them if they are violated. The last part of the previous sentence is extremely important. It is easier to write code that works as it should than it is to catch conditions that would make code fail. Handling errors can mean setting return values to default values, that is, silently handling errors. Or it could mean flagging an error condition and terminating. In C, without something called an "exception", the best way to handle errors is to return error values, ones that would not work correctly given the operating of the function in question.
Read a lot
Good coders get better in the same way as good writers: they read a lot. Reading good code helps you write good code. Just as practice coding makes you a better coder, reading other people's code helps you be more critical of your own code.
Let Others Read Your Code
Code reviews, the practice of presenting your code to others and/or letting others read through your code, is an excellent way to find problems in your code and to learn from the expertise of others.
When you are finished with some code or a complete application, allow another developer to look over the code. Consider questions like
- Are there obvious logic errors?
- Are all data cases fully implemented?
- Are there better ways to implement an algorithm?
- Does the code adhere to coding standards: clear, obvious, and simple?
Code reviews expose you to new ideas, new and better techniques, and faster ways to correct and well-written code. They are a great way to mentor others and have others mentor you. They allow you to share the load of writing effective code.
These are great bits of advice. Sticking to them can be difficult, especially if you don't have established habits.
There are software tools that can help you create and maintain high-quality software and acquire good software habits. We will outline them briefly here.
Back in Chapter 2, we introduced the idea of an integrated development environment, or IDE. An IDE merges several tools crucial to the software development process together into one software platform. Throughout this book, we have used the CloudPebble IDE as a way to write and experment with applications for the Pebble smartwatch. CloudPebble is specifically targeted to the Pebble smartwatch platform and combines an editor, a compiler, an emulator and an installer with cloud-based storage. It's a great example of an IDE.
There are other IDEs that integrate more advanced software tools to support quality code development. In particular, interactive debugging and code analysis are two tools that are useful. Version control is also a way to support experimentation with different versions of software and backing up these versions.
Debugging is characterized by the methods you use to find errors in your applications. Debugging could be very simple, like putting
printf() statements in your code to print the values of variables, or it could be complex, using a IDE's mechanisms for tracing and stopping code and analyze runtime properties. Interactive debugging is the use of an IDE to mix stepping through code with code analysis. An IDE can make this process easy; there are also command line tools that ease debugging.
As we discussed in Chapter 2, interactive debugging can take several forms. Breakpointing is the setting up of a stopping point where executing code will pause and allow you examine the values of variables and the state of other programming elements. Inspecting or watchpointing are ways of watching code execute -- even slowing the execution down -- so you can watch programming elements change at execution time. Profiling is a way of collecting statistics on sections of code and providing ways of pinpointing inefficient or broken coding elements. Tracing is a method of depicting which parts of code execution as used (and, conversely, finding parts of code that are not used).
While CloudPebble does not use interactive debugging, other tools included with the Pebble SDK do indeed implement this. Most of these tools are command-line based. The most widely used of these is GDB, or the Gnu Debugger. GDB is an interactive, text-based debugging application that works with the Pebble SDK and the smartwatch emulator. GDB implements all the interactive debugging elements above. It is an extremely useful application.
For more information on GDB see Pebble documentation.
Version control tools allow you to manage changes to your code files. It encourages tracking code changes and experimenting with multiple versions of the same software project. Version control enables collaborative software development, where several developers use the same code base and interact with each other about which parts of a project each is working on.
At its simplest level, a version control system is a backup copy of your code. Taking backups frequently and maintaining multiple backups allows for multiple versions of your project. However, this method can be quite inefficient, because each backup is likely to be almost identical to previous backups. In addition, if several developers were to collaborate on a project, permission and code sharing issues are likely to lead to mistakes and add complexity to simple backup copies.
Version control systems manage these issues. While VCS systems do indeed make backup copies, the copies are typically stored as a set of changes to file contents rather than as the files themselves. In addition, these systems manage multiple developers per project, allowing collaborative code use and managing the merging of code changes. These systems can automate this process, maintaining integrity of source code. Using a distributed system that tracks code changes by developer helps manage code ownership and responsibility.
There are several VCS in use today. The most popular is Git: a distributed version control system designed to maintain local code files for each developer coupled to remote code systems that link developers together. It uses concepts like branching to allow development on copies of code files while maintaining original code files untouched. Then merging code branches, sometimes from many separate developers, allows code files to be updated by many developers at once. Git code bases are used in this book.
There are many Git repository services available for use over the Internet. Github and BitBucket are two service available for use with Git.
Packages are a great way to take advantage of abstraction. They are collections of code resources that can be used without considering the source code or the algorithms they use. You include them with your software project and use them as you would externally referenced code files.