On Forests and Trees

When an English speaker is drowning in details that make the big picture hard to see, she might complain, “I can’t see the forest for the trees.”

image credit: Miguel Virkkunen Carvalho (Flickr)

It’s an odd expression, partly ironic and partly humorous. When I hear it, I sometimes think of my sister, who, after moving from Indiana to Utah, complained that the mountains were getting in the way of her view. (Her tongue was firmly in her cheek… :-)

The expression also describes an important problem of software engineering–one that a lot of engineers don’t understand well enough. It’s a problem with generalization.
Continue reading

Small Files Are Your Friends

Yesterday I was discussing refactoring priorities with a colleague who’s a brilliant engineer, and I happened to mention my strong desire for smaller files in our codebase. I told him that I thought .h and .cpp (or .py or .java or .whatever) files with thousands of lines were a problem.

He asked me why.

He told me that he wasn’t opposed to the idea, but he always felt like it was more of a stylistic choice than a true imperative for good code. And he was curious to see if I could convince him differently.

After I pondered his question for a while, I realized that some of my opinion really is traceable to prejudice. I usually use IDEs instead of vim/emacs, and I think that promotes click-back-and-forth-and-hyperlink-in-many-little-files instead of open-a-big-file-and-scroll. My compatriots that are more console-centric are just as smart and effective–maybe more. So I’ll write that part off.

However, I also found some arguments for the small-file principle that feel more substantive. Small files are your friends.

More small friends. Photo credit: miguelandresen (Flickr)

Named scopes and cognitive complexity

The case for small functions is more discussed than the case for small files, and it has been made by almost every luminary in computer science. My colleague immediately conceded it, and I won’t repeat it here–but I will claim that many of the same arguments apply to files as well, because files as well as functions are an important named scope in software development. This in turn suggests some constraints on files with respect to cognitive complexity.

Studies of memory and human attention consistently demonstrate that we think best about small sets. This fact is reflected by the amount of detail visible within any given named scope, both in programming and in other thought tasks. How many top-level menus in the average application? Colors in most cultures’ divisions of the rainbow? Parameters in an easy-to-understand function? Sections in the average book store? Steps in easy-to-follow driving directions? (There’s a whole field called cognitive ergonomics that explores why these questions always have similar answers.)

How many functions should we put in a reasonable file?

For me, 2 or 5 or 10 feels tractable. 50 feels excessive.

If a “good function” also respects the cognitive complexity constraints of the human brain–not being too big to read in a screen or two, for example–then you end up with a reasonable upper boundary on file sizes of, maybe, 500 or 1000 lines. (See Steve Yegge’s insightful rant about code size being an engineer’s worst enemy. He focuses on codebase size, but much of what he says applies just as well at the next level down.)

I suppose that this argument is weakened by the features of some IDEs, which collapse tangential code blocks, display treeviews of functions, and support lots of hypertext-style navigation. But not all programmers use the same IDEs, and not all interactions with code are IDE-driven; file size remains relevant. There’s a reason why C# created partial classes to improve on java’s lump-it-all-in-a-single-file constraint…

When humans try to remember more than their brains can fit, stuff falls out. Big files mean that coders have to mentally model relationships between stuff that’s separated by way too much screen real estate. This is a recipe for bugs. It is also a serious impediment to learnability.

Loose coupling and encapsulation

Files are a natural unit of coupling. In most programming languages, you can declare a construct (a variable, an internal function, or class) within a file, and have that construct be invisible to the outside world. This means there is a built-in temptation for functions and classes to bind more tightly when they’re in the same file, because they have access to common but private knowledge. By breaking large files apart, you remove the temptation, break unnecessary dependencies, and promote looser coupling.

Another way to say this is that file boundaries are an encapsulation barrier. Use them to hide data. (See my recent post about encapsulation as a simplicity strategy.)

Code reuse and testability

A consequence of files hiding data is that when you have a function that might be useful in a dozen different modules, but the function is buried in a large file with lots of dependencies extraneous to that function, reuse and testability are both frustrated. If the function is in a file of its own, it’s more discoverable, and it’s reusable and testable without extra baggage.

Link optimization

A C/C++ corollary to the file boundary issue has to do with linkers and binary sizes. In many cases, linkers remove unused functions at compilation unit level, rather than at the individual function level. A .c or .cpp file is either in or out, as a unit. This means that if you have a .cpp file with 50 functions in it, and you call only 1 of them, all 50 get linked into the final binary. The result is bloated binaries. So: smaller .cpp files ==> smaller binaries. (Before you flame me about linker optimizations, I will admit that some linkers get more granular, depending on which switches you use. But it’s surprising how hard it is to do better than what I’ve described. Experiment and comment with your results.)

Counter Argument

I suppose you could argue that by making lots of small files, you’re creating more complexity in directories, in makefiles or projects, and so forth. Is 250 files in a folder worse than 15? Doesn’t that violate the “cognitive complexity” guideline above?

My comeback is: use packages or subdirectories or libraries (another level of management). You can’t subdivide forever, but you don’t need to.

The bottom line for me is experiential, not theoretical. I nearly always have cruddy experiences in code bases where large files are common. Small files don’t guarantee pleasant and productive work, but big ones seem to go hand-in-hand with other problems. I find it telling that codebases with big files are also codebases where people lament the lack of comments the most, for example. Over the years, I’ve become convinced that a simple rule of thumb about keeping files small will pay off more handsomely than almost any other coding best practice.

Action Item

Leave a comment to tell me what you think. Am I making a mountain out of a molehill? Or do you feel strongly about small file sizes as well? Have I omitted any important pros and cons from the discussion?

Good Code Is Optimized

(Another post in my “What is ‘Good Code’?” series…)

Yes, optimized.

But for what?

A lot of programmers seem to think that raw speed of execution is the only possible answer. If pushed, they may admit it’s also possible to optimize for minimal memory usage or minimal size of executable. Compilers have switches for that.

Get out of the box. Photo credit: lel4nd (Flickr).

Emerson said, “A foolish consistency is the hobgoblin of little minds.” In modern terms, he was deploring the lazy instinct to accept established wisdom instead of thinking outside the box. And I think optimization is one of those topics where we need a larger vision.

What about optimizing for:

  • Speed of coding (sometimes programmer time is the most constrained resource…)?
  • Ease of use (often, low learning curve and productive users outweighs all other factors…)?
  • Speed of testing (sometimes provably correct is the most important success criterion…)?
  • Full utilization (the major promise of physical-to-virtual-to-cloud migration)?
  • Ease of understanding and maintenance?
  • Integration with external systems?

Selecting the criteria against which you optimize is more than a technical question. It’s a strategic one, driven by business and organizational goals. Programmers who relentlessly pursue speed of execution to the exclusion of other considerations are not doing their teams or their companies a favor.

Action Item

Optimize one function for ease of understanding and maintenance. Make a short list of how your choices were different than they might have been if you optimized for speed of execution.

Good Code Is Balanced

In my first post about what constitutes “good code,” I claimed we were dealing with a complex question. This is why I distrust short answers.

So many competing concerns must be balanced to achieve goodness:

  • Testability
  • Maintainability
  • Short-term revenue pressures
  • Long-term strategic value
  • Performance (many aspects)
  • Scalability (up, down, across)
  • Ease of use
  • Supportability
  • Conceptual integrity
  • Alignment with the skills, temperament, interests, and tools of the team that owns it
  • Cost vs. benefit (for some problems, quick and dirty is definitely “right”)
  • Simplicity (separation of concerns)

More items undoubtedly belong on the list. Quite a balancing act!

Someone’s got this “balance” thing down! Photo credit: joãokẽdal (Flickr).

Action Item

Pick a module, application, or subsystem that you know well, and grade its code according to how much its coders emphasize a few different dimensions (e.g., performance, testability, scalability, ease of use). Do you like the balance? Are any attributes being neglected?