Various Object-Oriented Design Dilemmas

    [ Concrete or Abstract or Tool ]

A concrete Box object may represent an actual on-screen box where if you adjust the x or y properties of the object, the on-screen box moves and if you remove or delete the Box object, the visible box goes away. An abstract Box object may simply be a container for an x,y,width, and height along with some methods to use to calculate things like overlap, area, or whatever. A Box tool may be an object which only needs to exist long enough to plot some pixels that represent a box on screen (or somewhere else) and then it is no longer needed.

    [ Adding functionality to an existing class/object ]

Some languages allow you to add methods (and maybe member variables) to existing classes without needing access to the original class’ source (Categories in Objective-C, for example). That’s dangerous at times and could yield undefined behavior if you happen to use a method name that was used within the class (but may not have been public) or was also added to the class by another module in your program.

Another option is to subclass the original class and add your new functionality there. This is ultimately more flexible and safer than simply extending an existing class, but there’s a variety of things that make it less than ideal. One big one is that if you’re trying to add functionality to a common library object such as an Array, you now need to re-wrap any arrays you may get as a result of some other methods that only used the standard Array object as a return value in order to use your new functionality. Some languages let you dynamically change the class of an object instance as a way of dealing with this situation. (Smalltalk comes to mind.)

Subclassing has another problem which is that if you’re intending to alter existing behavior and do not have knowledge of the inner workings of the object, chances are good that you cannot add the new functionality without potentially breaking or re-implementing features that the class already has. An example may be a Box object where you wish to add the option of rounding the corners. This box object may also have a variety of other properties already present such as defining line color, fill color, padding, gradients, etc - but the only public drawing-related method that’s visible to you is draw(). Since you can only knowingly override the draw() method, and do not have internal knowledge of the other methods within the class, it is likely that all of the padding, fill, line, etc. effects would need to be re-implemented by your subclass for the case of the rounded corners. Since you don’t know the intricacies of how all of the properties may interact (regardless of what may seem “logical” as viewed from the outside), the odds of doing this properly could be slim. It’s also a waste to have to re-imeplement logic you know is already done for you - if only you knew the name of the functions to call and in which order.

Similar to class extension mentioned at the start of this section, subclassing can also pose a problem when accidently re-defining a method that may have been used internally by the superclass. Without complete knowledge of the object’s internal definition, such a mistake is potentially easy to make. Some languages may offer various levels of protection against this.

If an object is designed for it, delegation can sometimes be used to extend the functionality of an object. Of course in this case you are at the mercy of the design of the original object. If you need to add functionality in a way that doesn’t match what the original author intended and provided hooks for, then you’re in trouble.

If an object is designed for it, it may be possible to extend the functionality of an object by passing in functions or other objects that act as “do-ers” of certain things. For example, the Box object mentioned previously may allow itself to be defined using a different “drawer” object which could possibly allow one to implement rounded corners. This is sort of like a “reverse delegation” in that instead of the object calling out to find additional functionality, it is handed self-contained “engines” of functionality to use later. I don’t believe I’ve ever tried to do things this way, though, as it feels like it’d be pretty clunky and hard to get right in a general way.

And finally, there’s the option of starting fresh with a new object or class and implementing only what you need within the context of your program. In the case of the rounded corners on the Box object, perhaps your program only needs an outline version of a rounded box. While the Box object you originally intended to extend can do fills, gradients, and a bunch of other fun stuff, it’s all things you don’t need right now so it is ultimately easier to make a new object that’s entirely disconnected from Box which only does what’s needed at the time. In my experience, this is what ends up happening most often, and it’s unfortunate, in a way. It seems to be counter to the ideals of object oriented design, but the realities of the process seem to present a significant barrier to “doing it right.”

    [ The black hole ]

After reading through the last section, it may be tempting to sit down and design a small class hierarchy that easily and clearly provides for good ways to share code and solve the example problem of adding a curved corner to a Box object with minimal effort and then shout to me, “Look! You’re wrong! It’s easy if only X, Y, and Z are changed!” Abstractly, it could be done, yes. The point, however, was that the Box object was closed to us. Perhaps it came from the OS vendor and there’s no source to examine or modify. You’re stuck with it as it is.

When faced with the reality that a given object that is not open for internal modification is causing pain, it’s easy to pretend it doesn’t exist and build a whole new object hierarchy that does exactly what’s needed in exactly the way you find the most comfortable and elegant. This might mean simply defining a new RoundedBox object that has nothing at all in common with the given Box object, or it might go so far as to re-define Box as MyBox and re-implement the drawing and features you need and do it in such a way that you can then create MyRoundedBox as a subclass of MyBox and get the job done in only a few lines of code.

Welcome to the black hole of object oriented design. Given enough time, virtually every vendor-provided object you need to extend has some missing functionality, unintended bug, strange performance characteristic, or whatever else that brings your progress to a halt and demands a work around. The workarounds start as hacks, then are re-factored into a “design” and, if you’re not careful, can start to take over your program entirely. Pretty soon you develop a new idiom that the other objects you use don’t obey but you’re convinced is so much better that it’s worth extending more objects, redefining others, and rebuilding the entire puzzle that is your program into a whole new image all in the name of “elegance” or “simplicity” or “extensibility.”

Inevitably, after spending countless hours implementing subtly different versions of functionality that you know already exists buried deep within the system (but which you may not have access to or is officially undocumented, etc) in the name of “design,” a requirement will change and what had only moments before been a perfectly engineered structure capable of withstanding anything becomes a house of cards trying not to collapse in the face of the coming hurricane.

As programmer, you may spend hours upon hours seemingly getting things done. As the project gets larger, “small” changes require ever greater programming effort and “simple” design tweaks ripple through the entire system. From the outside your progress appears to become slower and slower as you sink farther into the black hole - forever not quite arriving.

Comments are closed.