by Matt Slot
Object oriented programming, or OOP, has spawned a number of buzzwords promising better use of resources: automatic classes, polymorphism, reference counting, etc. More and more programmers are moving to C++, Java, and other objective languages so that they can build application frameworks and reusable object libraries. What gripe could I have with that?
Classes and multiple inheritance have always been the foundation of C++, but it's been difficult to keep up with the flurry of improvements and changes the language has suffered. Exceptions and templates are powerful but easy to abuse features, hence the need for more than language reference manuals. The flurry of books with titles like "Effective C++" is the logical consequence of extending a language with features that are easy to grasp, but difficult to use properly.
Now, I'm already an old curmudgeon at 30, so I prefer having fine control over the code I write and the output it generates. Of course, a high level language reduces the amount of work necessary for common operations, but it comes at the cost of precision. For example, simple operations like saving, initializing, and restoring state can be encapsulated into an automatic class, so that object construction and destruction perform the work inline.
But there are hidden dangers with automatic classes. Inline operations may be slow or memory intensive, and can cause subtle side effects when "hidden" code interacts with explicit application code. Next, it's often necessary to save and restore more context than a given function touches, because the class must be general enough to use in the simple and complex cases. Finally, pushing and popping state data may require memory allocation, which may fail inconveniently inside the automatic constructor.
We often scavenge snippets from previous projects instead of rewriting the same code from scratch. Reusing an existing foundation of common classes means that we no longer need to rewrite the same mundane code for dragging windows, handling events, and tracking errors. Even products that must be maintained and upgraded several times benefit from a flexible object framework -- adding utility windows and dialogs is simply an exercise in subclassing.
There are problems though. The first being that most useful frameworks are quite large. In addition to learning the native platform APIs, you must also learn the hierarchy and usage conventions of the core classes. It's possible to quickly write a "hello world" application using C, but frameworks have a steeper learning curve. Careful thought is often necessary before adding a new feature to a framework, so that it is consistent with the existing class design.
It's not only about learning the names and locations of the classes, but also the "design policies" of the original designer. Which code is responsible for deleting an object, the caller or the object itself? How is data tracked internally, using STL or private utility classes? Where are errors detected, how are they propogated to the caller, and who tells the user when something fails?
On the implementation side, it's much more difficult to modify portions of a complex class hierarchy because of the formal and implicit dependencies. Whereas in C you could quickly hardwire separate parts of the code to try something new, C++ enforces restrictions laid down by the original author. Of course, even a perfectly designed base class can be subverted when derived classes make assumptions about callback and usage conventions.
Polymorphism causes it's own problems. An abstract base class contains common code, which is subclassed into different but related features -- for example a window class with derived document windows, floating toolbars, and modal dialogs. In this case, the code to manage windows is spread among 4 different locations, making debugging window layering and interaction much more difficult.
Overengineering a framework for use in some mythical future project is often a waste of time, because you simply can't predict the future. Don't spend weeks writing extremely general class libraries if you don't need them right now. Invariably you'll find fault with a fundamental design decision, and you'll need to rewrite those parts of the code anyway. If you can reuse over 50% of the framework without modification, you'll be lucky.
In the end, any software design is based on certain assumptions, and as code accumulates, more assumptions and subtle interactions are added. The complexity of a project increases exponentially with it's size (yes, even those written in C or LISP). In an ideal world, a framework would be designed from the start to support any future enhancement -- but that's just not practical. While OOP helps formalize software design and object interaction, sometimes the programmer must bend the framework a bit to meet his needs.
I admit that the blame rests on the overwhelming amount of code developers must manage, and not just OOP or C++ -- but until software development becomes an entirely automatic process, we developers will have to deal with the complexity of our creations.
While OOP is one tool at our disposal, it's certainly not a cure all. Increased size and complexity are a natural consequence of software evolution, but misapplying (or overapplying) the OOP design philosophy is a slippery slope toward software bloat. It's said that when you have a hammer, everything starts to look like a nail. Part of knowing how to use a hammer is knowing when to set it aside and pick up the screwdriver.
Matt Slot, Bitwise Operator