…and it was good.
Why is it that code starts out nice and deteriorates over time?
What happened happened to make the code badness grow? Probably something joyous like the first order for a new product or adding people to the team. Or maybe something sad like losing a key employee.
I see a lot of legacy C code. How does a function get to be 1000 lines long? How did that file get to be 40 KLOC or 100 KLOC? A friend showed me a 27 page C function in some telecom code. Amazingly enough, it worked! How does code get that way?
One Copy/Paste at a time!
Long functions often contain deep nesting, complex conditional logic, special case handling and the list goes on. The functions do their work with primitive data types and access to global data structures. They loop over collections of entities as they manipulate each entity. A bug is reported, a special case is added.
In short, long functions are doing too much! Your code did not start that way. With each change, functions collect more responsibility. The growth is insidious and hardly noticed until it is a serious problem.
(In all fairness, you probably can find a few long functions that should be long. Those are a tiny minority of the population of long functions. )
Developers are often quick to blame unreasonable deadlines and their managers and the people before them for their poor code.
Some blame may be appropriate. The pressure is real. Programmers are in a hurry. They hear “just get it working”. Maybe your company has institutionalized creating bad code. Bad code costs a lot, now and in the future.
Some programmers may not know better. Getting code to work, is essential, though by itself it is not sustainable.
Programmers Cannot Just Blame Management
Getting code to work is what I call the programmer’s App-titude Test. If you can get the App to work (warning: this is not easy), you may have the aptitude to be a professional programmer. Not only does the code have to work, it needs to be written to make it easier to understand and change.
As a programmer, I cannot just blame management. I need to take responsibility for my product, my behavior, my skills. Team’s I’ve worked with pretty quickly can learn to identify code smells. What to do about the smell is usually harder. Improving the structure without breaking the system is also difficult. Just the same, like your kitchen, you need to regularly clean up.
With the business side not knowing better and stressing that development just get the code out, coupled with no clear vision and path to better code, the cycle continues.
Why Do Functions Grow?
I was with a group of developers helping them learn Test-Driven Development. Many were struggling while defining a new C++ class. It’s C++ so it is kind of understandable. (I struggle with it too, that’s why I like to go carefully with a tight feedback loop.) I asked “when was the last time you defined a new C module or C++ class?” It had been at least 6 months. I then asked, “Who of you writes code every week?”. Most the hands went up. “Where do you put that code?” As it turns out, they wedge their changes right into the existing code, not even considering adding new structure as the needs change.
This behavior means functions gradually collect more to do as a system encounters the real world. There are new features, new special cases, and slight variations in behavior that require code to be changed.
A common thing that happens is that a function’s local data grows, as functionality grows. Local variables act like glue, cementing the lines of a function together. The cleanup effort inevitably involves extracting functions. The local variable glue shows up in the extracted functions as another code smell: duplicate and long parameter list. This smell is actually great news if you can understand what the code is telling you, you need a new abstract data type! You found a separable responsibility that can be extracted to a new module.
Why does it matter?
It matters because developers spend most their time reading code. Long functions are usually hard to understand. When functions are hard to understand you cannot quickly determine if you are in the right place. So you dig deeper. The hunt goes on. You make your change. You check the new functionality and hope there are no unintended consequences.
What can we do?
Changing existing code is risky. For good reasons, programmers are often afraid to clean up their code. (BTW, they should be unless they have a good test suite.) That fear leads to leaving the code as is. Code got bad incrementally, you need to start to improve it incrementally. What if every time you touch the code it gets better instead of worse.
I’m talking about Bob Martin’s Boy Scout Rule. It is hard but rewarding work. Over time your code base can improve. You don’t have to believe me, read these Stories from the Field from people have saved their product’s code.
C and C++ programmers can get help from my book Test-Driven Development for Embedded C). It could help you get started with unit testing, refactoring and design. Adding tests to existing C can be a big challenge, especially embedded C. Here is my recipe of adding tests to legacy C.
Let me know if you need any help.