Design Fundamentals is my series on code design aimed at people who may have done some code in school, but haven’t done much code design, or who haven’t read much about design before. This is the second article of the series.
Code design is all about making sense of a system. Any software worth mentioning quickly grows larger than your brain can easily track. A good design will ensure that each piece of the program can be safely worked on in isolation — that whatever you need to do, it’ll fit within your working memory.
Abstraction is a key component to keeping the amount of details you need to remember at once at a minimum. By abstracting a piece of code, you remove the details and only have to think about the abstract interface to the code, which should be smaller (or the abstraction was a bad idea).
So exactly how does abstraction work? I’m sure you’ve done some version of it before, maybe even without being aware of it. At a low level of abstraction, the code is filled with all the details. At a high level of abstraction, code uses concepts that hide details. So in abstracting code, we lift out the details to some other place — maybe a class or a function.
When to abstract
While you can debate any kind of rules for when to abstract (just like most things in code), I’ll attempt to give you some guidelines. Adapt them as they suit you and change them if they don’t make sense in your context.
For functions, what you can keep in your head is strongly related to what you can see on your screen. As soon as a function has you scrolling up and down, you’ve got a good sign that the function is too long and that you should abstract something away. For me, the maximum length of a function that is normally reasonable is about 50 lines — the exact number will vary with personal preference and the content of the function.
For both functions and classes, the relationship with other classes or functions also matters — specifically the fan-out (amount of other classes your class uses, for instance). You want to keep the fan-out low, since otherwise you risk forgetting the details of something while working on the code (or confusing the next coder who works with it). Steve McConnell has this to say about it in Code Complete (a recommended book if you’re interested in code and design):
Low-to-medium fan-out Low-to-medium fan-out means having a given class use a low-to-medium number of other classes. High fan-out (more than about seven) indicates that a class uses a large number of other classes and may therefore be overly complex. Researchers have found that the principle of low fan-out us beneficial whether you’re considering the number of routines called from within a routine or from within a class.
The same thing applies with the number seven as with the above line count — use your judgement, but keep in mind when the fan-out is starting to increase that you might want to look at your design again.
Common abstraction mistakes
The number one mistake people do when it comes to abstraction is to confuse the need to abstract with the interface of classes that is exposed to others. Often, when abstracting a part of a function to a new function, it doesn’t need to go into the interface of the class you’re working with at all — it can simply be placed in an anonymous namespace at the top of the file you’re working on.
Another mistake that is fairly common is to treat abstraction as a simple cut and paste activity, where a number of lines of code are simply moved to another place. Stop to consider where to make the cut — what part of the code you’re working on is a logical unit that does something reasonable? If you end up with a function named partOfMyOtherFunction() or something similar, you haven’t really achieved anything.
Keep thinking about your abstracted functions — do they share characteristics? Often you might find yourself creating helper functions that would fit better grouped together in a helper class. This class can also reside in the same file — there’s nothing which forces you to create a new header file and source file in order to abstract code.
Building abstract code
If possible, build your code in abstract layers from the start. Consider what parts you might need when implementing your functionality, and make sure implementation details are well partitioned off. Especially things like system differences and interfaces towards the operating system is good targets for an abstraction layer.
Encapsulate the system functionality in order to not have to bother with details everywhere in the code. This is especially useful when interfacing with badly designed APIs, to ensure that this doesn’t spread to all of your code. This lets the rest of your code think about the system functionality at a higher level, without the details.