Awesome Functions
 |
For hints, tricks and ideas about better ways to build embedded systems, subscribe to The Embedded Muse, a free biweekly e-newsletter. No hype, just down to earth embedded talk. 23,000 other engineers subscribe. It takes just a few seconds (all we need is your email address, which is shared with absolutely no one) to subscribe to the Embedded Muse. |
Awesome Functions
It's possible to write ugly, obfuscated, horribly
convoluted and undocumented code that works. God knows there's an awful lot of
it out there today, controlling everything from disposable consumer products to
mission-critical avionics. One reader who works for an ABS brake company tells
me their code is "absolutely hideous", which makes the thought of buying a
1971 VW Beetle appealing.
But it's equally possible, and quite a bit easier, to
write finely-crafted functions whose inherent correctness shows through, and
which are intrinsically maintainable. Here are some guidelines.
Minimize Functionality
Are you an EE? My informal surveys suggest that around
60-70% of all firmware folks have electrical engineering degrees. That
background serves us well in understanding both the physics of our applications
as well as the intricacies of the hardware we control.
Yet most EE curriculums ignore software engineering. Sure,
the professors teach you to program, and expect each student to be very
proficient at building code. But they provide a didactic devoid of the critical
tenants of software engineering necessary to building large, reliable, systems.
The skills needed to create a working 500 line program do not scale to one of
100,000 lines.
Probably the most well-known yet least used rule of
software design is to keep functions short. In my firmware lectures I ask how
many attendees have an enforced
function size limitation. It's rare to find even a single hand raised. Yet we
know that good code is virtually impossible with long routines.
If you write a function that exceeds 50 lines - one page
- it's too long. Fact is, you probably cannot remember a string of more than
8 or 10 numeric digits for more than a minute or so; how can you expect to
comprehend the thousands of ASCII characters that make up a long function?
Worse, trying to follow program flow that extends across page boundaries is
tough to impossible, as we flip pages back and forth to understand what a gaggle
of nested loops are doing.
Keep functions short. That implies that a function should
do just one thing. Convoluted code that struggles to manage many disparate
activities is too complex to be reliable or maintainable. I see far too many
functions that take 50 arguments that select a dozen interacting modes. Few of
these work well.
Express independent ideas independently, each in its own
crystal clear function. One rule of thumb is that if you have trouble
determining a meaningful name for a function, it's probably doing too many
different things.
Encapsulate
OOP advocates chant the object mantra of "encapsulation,
inheritance and polymorphism" like Krishna devotees. OOP is a useful tool that
can solve a lot of problems, but it's not the only tool we possess. It's not
appropriate for all applications.
But encapsulation is. If you're so ROM-limited that every
byte counts encapsulation may be impossible. But recognize that those
applications are inherently very expensive to build. Obviously in a few
extremely cost-sensitive applications - like an electronic greeting card -
it's critically important to absolutely minimize memory needs. But if you get
into a byte-limited situation figure development costs will skyrocket.
Encapsulation means binding the data and the code that
operates on that data into a single homogeneous entity. It means no other bit of
code can directly access that data.
Encapsulation is possible in C++ and in Java. It's
equally available to C and assembly programmers. Define variables within the
functions that use them, and insure their scope limits their availability to
other routines.
Encapsulation is more than data hiding. A properly
encapsulated object or function has high cohesion. It accomplishes its mission
completely, without doing unrelated activities. It's exception-safe and
thread-safe. The object or function is a completely functional black box
requiring little or no external support.
A serial handler might require an ISR to transfer received
characters into a circular buffer, a "get_data" routine that extracts data
from the data structure, and an "is_data_available"
function that tests for received characters. It also handles buffer overrun,
serial dropout, parity errors, and all other possible error conditions. It's
reentrant so other interrupts don't corrupt its data.
A collarary of embracing encapsulation is to delete
dependencies. High cohesion must be accompanied by low coupling - little
dependence on other activities. We've all read code where some seemingly
simple action is intertwined in the code of a dozen other modules. The simplest
design change requires chasing variables and functionality throughout thousands
of lines of code, a task sure to drive maintainers to drink. I see them on the
street here in Baltimore by the dozen, poor souls huddling in inadequate coats,
hungry and unshaven, panhandling for applets. If only they'd "said no" to
the temptation of global variables.
Remove Redundancies
Get rid of redundant code. Researchers at Stanford studied
1.6 million lines of Linux and found that redundancies, even when harmless,
highly correlate with bugs. (See www. stanford.edu/~engler/p401-xie.pdf).
They defined redundancies as code snippets that have no
effect, like assigning a variable to itself, initializing or setting a variable
and then never using that value, dead code, or complex conditionals where a
subexpression will never be evaluated, since its logic is already part of a
prior subexpression. They were clever enough to eliminate special cases like
setting a memory mapped I/O port, since this sort of operation looks redundant
but isn't.
Even harmless redundancies that don't create bugs are
problems, since these functions are 50% more likely to contain hard errors than
other functions that do not have redundant code. Redundancy suggests the
developers are confused, and so are likely to make a series of mistakes.
Watch out for block-copied code. I am a great believer in
reuse, and encourage the use of previously-tested chunks of source. But all too
often developers copy code without studying all of the implications. Are you
really sure all of the variables are initialized as expected, even when this
chunk is in a different part of the program? Will a subtle assumption about a
mutex create a priority inversion problem?
We copy code to save development time, but remember there
is a cost involved. Study that code even more
carefully than the new stuff you're writing from scratch. And when Lint
or the compiler warns about unused variables, take heed: this may be a signal
there are other, more significant, errors lurking.
Reduce Real-Time Code
Real-time code is error-prone, expensive to write, and even
more costly to debug. If it's at all possible, move the time-critical sections
to a separate task or section of the program. When time issues infiltrate the
entire program then every bit of the program will be hard to debug.
Today we're building bigger and more complex systems than
ever, with debugging tools whose capabilities are less than those we had a
decade ago. Before processor speeds zoomed to near infinity and the
proliferation of SMT packages eliminated the ability of coffee drinkers to probe
ICs, the debugger of choice was an in-circuit emulator. These included real-time
trace circuits, event timers, and even performance analyzers. Today we're
saddled with BDM or JTAG debuggers. Though nice for working on procedural
problems, they offer essentially no resources for dealing with problems in the
time domain.
Remember also two rules of thumb: a system loaded to 90%
doubles development time over one at 70% or less. At 95% the schedule triples.
Real-time development projects are expensive; highly loaded ones even more so.
Flow With Grace
Flow, don't jump. Avoid continues, gotos, breaks and
early returns. These are all useful constructs, but generally reduce a
function's clarity. Overused, they are the basic fabric of spaghetti code.
Refactor Relentlessly
XP and other agile methods emphasize the importance of
refactoring, or rewriting crummy code. This is not really a new concept, Capers
Jones, Barry Boehm and others have shown that badly-written modules are much
more expensive to beat into submission and maintain than ones with a beautiful
structure.
Refactoring zealots demand we rewrite any code that can be
improved. That's going too far, in my opinion. Our job is to create a viable
product in a profitable way; perfection can never be a goal that overrides all
other considerations. Yet some functions are so awful they must be rewritten.
If you're afraid to edit a function, if it breaks every
time you modify a comment, then it needs to be refactored. Your finely-honed
sense as a professional developer that, well, we just better leave this
particular chunk of code intact because no one dares mess with it, is a signal
that it's time to drop everything else and rewrite the code so it's
understandable and maintainable.
The second law of thermodynamics tells us that any closed
system will head to more disorder; that is, its entropy will increase. A program
obeys this depressing truth. Successive maintenance cycles always increases the
software's fragility, making each additional change that much more difficult.
As Ron Jeffries pointed out, maintenance without refactoring increases
the code's entropy by adding a "mess" factor (m) to each release. The cost
to produce each release looks something like: (1+m)(1+m)(1+m)!., or (1+m)
12.0pt">n, where n is the number of releases. Maintenance costs
grow exponentially as we grapple with more and more hacks and sloppy shortcuts.
This explains that bit of programmer wisdom that infuriates management: "the
program is too much of a mess to maintain".
Refactoring incurs its own cost, r. But it eliminates the
mess factor, so releases cost 1+r+r+r!, which is linear.
Luke Hohmann advocates "post release entropy
reduction." He recognizes that all too often we make some quick hacks to get
the product out the door. These entail a maintenance cost, so it's critical we
pay off the technical debt incurred in being abusive to the software.
Maintenance is more than cramming in new features; it's also reducing accrued
entropy.
Refactor to sharpen fuzzy logic. If the code is a
convoluted mess or just not absolutely clear, rewrite it so it better
demonstrates its meaning. Eliminate deeply nested loops or conditionals - no
one is smart enough to understand all permutations of IFs nested 5 levels deep.
Clarity leads to accuracy.
Employ Standards and Inspections
Write code using your company's firmware standard. Use
formal code inspections to insure adherence to the standard and to find bugs.
Test only after conducting an inspection.
Inspections are some 20 times cheaper at finding bugs than
traditional debugging. They'll capture entire classes of problems you'll
never pick up with conventional testing. Most studies show that traditional
debugging checks only about half the code! Without inspections you're most
likely shipping a bug-ridden product. It's interesting that the DO-178B
standards for building safety critical software rely heavily on the use of tools
to insure every line of code gets executed. These code coverage tools are a
wonder, but are no substitute for inspections.
I've written much about both of these issues, so refer
you to (http://embedded.com/98/9808br.htm) and (March 1998 and April 1998) and
(http://embedded.com/story/OEG20020221S0084). But it's important to emphasize
how standards and inspections are tightly bound to each other; neither will
succeed without the other.
Without standards and inspections it's just not possible
to habitually build great firmware products.
Summary
When I was just starting my career an older fellow told me
what he called The Fundamental Rule of Engineering: if the damn thing works at
all, leave it alone. It's an appealing concept, one I used too many times over
the years. The rule seems to work with hardware design, but is a disaster when
applied to firmware. I believe that part of the software crisis stems from a
belief that "pretty much works" is a measure of success. Professionals,
though, understand that developmental requirements are just as important as
functional and operational requirements. Make it work, make it beautiful, and
make it maintainable.
|