Code Review 2: Hiding Implementation DetailsEmbedded classes, protected constructors, hiding constants, anonymous enums, namespaces. | |||||||||||||||||
![]() source |
A good software engineer is like a spy. He exposes information to his collaborators on the need-to-know basis, because knowing too much may get them in trouble. I’m not warning here about the dangers of industrial espionage. I’m talking about the constant struggle with complexity. The more details are hidden, the simpler it is to understand what’s going on. Using Embedded ClassesThe class Link is only used internally by the linked list and its friend the sequencer. Frankly, nobody else should even know about its existence. The potential for code reuse of the class Link outside of the class List is minimal. So why don’t we hide the definition of Link inside the private section of the class definition of List.
The syntax of class embedding is self-explanatory. Class ListSeq has a data member that is a pointer to Link. Being a friend of List, it has no problem accessing the private definition of class Link. However, it has to qualify the name Link with the name of the enclosing class List--the new name becomes List::Link.
The classes List and ListSeq went through some additional privatization (in the case of ListSeq, it should probably be called "protectization"). I made the GetHead method private, but I made ListSeq a friend, so it can still call it. I also made the constructor of ListSeq protected, because we never create it in our program--we only use objects of the derived class, IdSeq. I might have gone too far with privatization here, making these classes more difficult to reuse. It's important, however, to know how far you can go and make an informed decision when to stop. Combining ClassesConceptually, the sequencer object is very closely tied to the list object. This relationship is somehow reflected in our code by having ListSeq be a friend of List. But we can do much better than that--we can embed the sequencer class inside the list class. This time, however, we don't want to make it private--we want the clients of List to be able to use it. As you know, ouside of the embedding class, the client may only access the embedded class by prefixing it with the name of the outer class. In this case, the (scope-resolution) prefix would be List::. It makes sense then to shorten the name of the embedded class to Seq. On the outside it will be seen as List::Seq, and on the inside (of List) there is no danger of name conflict. Here's the modified declaration of List:
Notice, by the way, how I declared Seq to be a friend of List following its class declaration. At that point the compiler knows that Seq is a class. The only client of our sequencer is the hash table sequencer, IdSeq. We have to modify its definition accordingly.
And, while we're at it, how about moving this class definition where it belongs, inside the class HTable? As before, we can shorten its name to Seq and export it as HTable::Seq. And here's how we will use it inside SymbolTable::Find
Combining Things using NamespacesThere is one more example of a set of related entities that we would like to combine more tightly in our program. But this time it's a mixture of classes and data. I'm talking about the whole complex of FunctionTable, FunctionEntry and FunctionArray (add to it also the definition of CoTan which is never used outside of the context of the function table). Of course, I could embed FunctionEntry inside FunctionTable, make CoTan a static method and declare FunctionArray a static member (we discussed this option earlier). There is however a better solution. In C++ we can create a higher-level grouping called a namespace. Just look at the names of objects we're trying to combine. Except for CoTan, they all share the same prefix, Function. So let's call our namespace Function and start by embedding the class definition of Table (formerly known as FunctionTable) in it.
The beauty of a namespace it that you can continue it in the implementation file. Here's the condensed version of the file funtab.cpp:
As you might have guessed, the next step is to replace all occurrences of FunctionTable in the rest of the program by Function::Table. The only tricky part is the forward declaration in the header parse.h. You can't just say class Function::Table;, because the compiler hasn't seen the declaration of the Function namespace (remember, the point of using a forward declaration was to avoid including funtab.h). We have to tell the compiler not only that Table is a class, but also that it's declared inside the Function namespace. Here's how we do it:
By the way, the whole C++ Standard Library is enclosed in a namespace. Its name is std. Now you understand these prefixes std:: in front of cin, cout, endl, etc. (You've also learned how to avoid these prefixes using the using keyword.) Hiding Constants in EnumerationsThere are several constants in our program that are specific to the implementation of certain classes. It would be natural to hide the definitions of these constants inside the definitions of classes that use them. It turns out that we can do it using enums. We don’t even have to give names to enums—they can be anonymous. Look how many ways of defining constants there are in C++. There is the old-style C #define preprocessor macro, there is a type-safe global const and, finally, there is the minimum-scope enum. Which one is the best? It all depends on type. If you need a typed constant, say, a double or a (user defined) Vector, use a global const. If you just need a generic integral type constant—as in the case of an array bound—look at its scope. If it’s needed by one class only, or a closely related group of classes, use an enum. If its scope is larger, use a global const int. A #define is a hack that can be used to bypass type checking or avoid type conversions--avoid it at all costs. By the way, debuggers don’t see the names of constants introduced through #defines. They appear as literal numerical values. It might be a problem sometimes when you don’t remember what that 74 stood for. Here’s the first example of hiding constants using enumerations. The constant idNotFound is specific to the SymbolTable.
No change is required in the implementation of Find. Being a method of SymbolTable, Find can access idNotFound with no qualifications. Not so with the parser. It can still use the constant idNotFound, since it is defined in the public section of SymbolTable, but it has to qualify its name with the name of the class where it's embedded.
Maximum symbol length might be considered an internal limitation of the Scanner (although one might argue that it is a limitation of the "language" that it recognizes--we’ll actually remove this limitation later).
Hiding Constants in Local VariablesA constant that is only used within a single code fragment should not, in general, be exposed in the global scope. It can as well be defined within the scope of its usefulness. The compiler will still do inlining of such constants (that is, it will substitute the occurrences of the constant name with its literal value, rather than introducing a separate variable in memory). We have a few such constants that are only used in main
|