Previous section   Next section

Imperfect C++ Practical Solutions for Real-Life Programming
By Matthew Wilson
Table of Contents
Chapter 20.  Shims


20.10. Breaking Up the Monolith

One of the biggest problems in object-oriented design, for most object-oriented languages, but especially for C++, is the business of determining the level of encapsulation of the operations for a given type. For some types it can be impossible to come to an optimal balance between the requirements of encapsulation and usability. Consider the example of a file.

If we encapsulate a system file handle within a class, we will provide operations to open, close, read-from, and write-to the file. What if we then want to interface with another API that uses a file? For example, we might want to use memory-mapped I/O. We have two choices. One option is to include memory-mapping operations in our file class. We're then on a slippery slope. We might subsequently want to, say, use the file handle on Win32 in combination with an IO Completion Port [Rich1997]. Soon our file class is becoming more of an I/O class. What's next—methods for opening sockets and manipulating IP addresses?

A second approach is to have a class framework, and to declare various classes as friends, so MemoryMap and CompletionPort classes would be declared as friends of our File class. I barely need to comment that this increases the physical coupling [Lako1996] and the logical fragility of the entire framework. Not to mention the nightmare that becomes the binary distribution of the framework. We've seen notable examples of just how bad this approach can get in the real world. This problem's not just restricted to C++. Although there are several good examples of this bad situation in C++ libraries, there are better examples in languages that are supposed evolutionary successors of C++.

A third approach is to have the underlying resource handle be publicly accessible. The problem here, of course, is that it is far too easy to break the encapsulation, since the only thing keeping it is the assumption that all developers will understand the details of any such direct access that they make, and will not make any mistakes. Since even the best developers make simple mistakes, this is a reckless approach.

The final approach is to have read-only implicit conversion operators. This is no less horrible than the others. You can close a file handle behind the back of the File class instance. Need I say more?

There's no wonder, when you consider these options, that many programmers favor using C-APIs, or prefer to stick to standard C++ where all these egregious interdependencies are "hidden" inside, or at least not commented upon. The fact is, there is no right approach here, and I'm not going to try to pretend that there is one. Wherever possible, you are going to have more success by keeping your classes small and simple. But there will inevitably be compromises.

What I can tell you, though, is that shims are extremely helpful in this regard. Using shims you can write classes that can interoperate, but that do not need to be aware of each other's natures in any way. You do not need to write monolithic classes, or monolithic class frameworks (i.e., you can leave the friend declarations at home); you do not need to make members publicly accessible; you do not need to provide implicit conversion operators. You will still have to compromise and make the underlying resource handles accessible, but this can be via shims whose invocation is never implicit. Thus, I would write a File class that had a simple set of operations and that provided its internal handle—int on UNIX; HANDLE on Win32—via a get_handle() shim. Then the MemoryMap class would be defined with a template constructor that uses the get_handle() shim to elicit the file handle associated with its argument, as in:



class MemoryMap


{


public:


  template <typename F>


  explicit MemoryMap(F f)


    : m_f(get_handle(f))


  {


    . . .



And so the classes can interoperate without knowing anything about each other:



File        f("/ImperfectsC++/readme.txt");


MemoryMap   mm(f);



Naturally, there's nothing stopping someone from making pathological calls:



File        f("/ImperfectsC++/readme.txt");


close(get_handle(f)); // Now f's dtor will crash!



But then there's nothing stopping anyone from casting an int to a vector<string>* when you get down to it; if you want to have an elephant in your drawing room, I'm not going to tell it to pack its trunk!


      Previous section   Next section