Abstraction layers are the easiest, most general-purpose way of making code more portable. The basic concept is well discussed in computer science textbooks. However, most people still find themselves unclear on when it is appropriate to add an additional layer of abstraction.
The most straightforward rule of abstraction is that you should split functionality by use. If a block of code is likely to be reused, even if the code needs to be slightly modified or wrapped in different code, that block should be a separate function. If a block of code is only relevant to the function in which it is enclosed, it probably should not be, unless doing so represents a significant benefit to readability.
You’d be surprised how well this single rule fits most situations. Take file access, for example. Say you want to take advantage of the Carbon file API (to get alias support, for example). The code to open, read, and write a file is used in many places in most applications. Therefore, it should be abstracted into its own function and called from those places.
What you should generally avoid doing, though, is abstracting the open, read, and write calls individually, then using them inside large loops. This leads to needlessly unreadable code with lots of very small abstraction layer functions. You should instead present an interface that makes sense in the context of your application.
For example, if your application performs streaming, you might have the following overall structure:
A function that opens a file
A function that reads the next n bytes that returns to a buffer containing the data
A function that rewinds the stream by n bytes
A function that closes the file
Your open function might return an opaque handle that can be passed around within the core of the application but whose structure is unknown outside the file functions.
Similarly, you might have functions that read and write the preferences file using a large data structure, a function to read the header of a file, and so on.
Avoid Conditionalizing Code
GUI Abstraction Issues
Other General Rules
Don’t fall into the trap of conditionalizing hundreds of bits of code with #ifdef directives. (An occasional #ifdef is OK.) This quickly leads to unmanageable code, particularly when you support Mac OS X, multiple UNIX-based and UNIX-like systems, Classic Mac OS, and Windows.
If you study the spots in your code that you need to special-case, more often than not, you will find that their issues are closely related. For example, they might all be graphics routines. If so, you might pull all the functions that call graphics routines into their own file and conditionalize the inclusion of the entire file. For example, you might use using one file for X11 routines, one for Win32 routines, and one for Carbon or Cocoa routines. It is easier to conditionalize code in a few core functions than to conditionalize it in a hundred random places in the code. Similarly, it is easier to replace an entire file than to replace bits of a file.
Most non-GUI abstraction layers should be as thin as possible, since a thick abstraction layer tends to lead to significant platform divergence. However, with a GUI, platform divergence is desirable and is thus an exception to this rule.
If you want your application to look identical on all platforms, then you should make the abstraction layer thin. However, this tends to result in Mac OS X users throwing your application in a dumpster because it doesn’t feel like a Mac application. Mac OS X has common GUI style rules that most X11 applications don’t follow.
For example, X11 applications often have per-window menus, while Mac OS X has per-application menus (with the ability to add or remove menus when a different window is in the foreground). X11 applications tend to have very square features and small, space-efficient buttons, while Mac OS X applications tend to have rounded, large, easier-to-read buttons.
Similarly, other operating systems, such as Windows and Classic Mac OS, have different design rules. You must understand the GUI design rules for each computing platform or your application will not be accepted easily on those platforms.
For this reason, it is easiest to split your entire user interface into a separate file or directory for X11, then clone that and rewrite it for Carbon or Cocoa when porting to Mac OS X. That way, you can heavily redesign the Mac OS X interface to match what Mac users expect without annoying your UNIX users.
A good way to implement this is to write your GUI to operate in a separate thread (or multiple threads, if desired). It can then communicate with the core of the application using pipes or other platform-specific solutions.
Another way to implement this is to simply have the GUI call functions in the core of the code. This design is effective when the core of the application is entirely GUI driven, but tends to result in the GUI appearing to wedge if the core of the application can take a long time to complete an operation, and is particularly hard to implement if the core code needs to do something continuously in the background during normal operation. You should consider these issues in your redesign to make your application more palatable to end users.
Don’t fall into the trap of overly abstracting your code. If a block of code will not be used in multiple places, don’t abstract it out unless it is platform-specific. Abstracting out code that appears in two or three places is, for readability reasons, usually not worth splitting into a separate function.
Do try to limit your functions to a reasonable length. Abstraction for readability alone is probably not a good idea, as splitting a function unnecessarily can end up making it harder to read. If the purpose of a function doesn’t divide in an obvious way into multiple subtasks, it is probably better not to split it. In many cases, though, you may find that an inner loop within that function can be considered a distinct operation unto itself. If so, that loop may be split into a separate function if doing so improves readability.
Don’t make abstraction layers more than five or six layers deep, as measured from the top level of a major functional unit. The deeper the nesting, the harder it is to follow the code when debugging.
Don’t make functions shorter than about ten lines of code. The readability improvement of such a small change in the outer function is rarely significant. There are, of course, exceptions, such as inline assembly (which should always be abstracted). These exceptions are good candidates for an inline function (or even a macro, if you are so inclined).
Remember that these are guidelines, not rules. You should generally follow your instincts. If you feel like something might be reusable, go ahead and abstract it even if it is only being used in one place. If you feel that splitting an inner loop into its own function is pointless, don’t split it. This applies to all guidelines, not just the ones in this chapter.
Last updated: 2008-04-08