One Reason Nick Hodges Doesn’t Quite Get OOP

Nick Hodges has written an entertaining article on what he percieves as the failings of the Microsoft .NET team’s attempt to design and code an object-oriented famework. Along the way he takes a few additional swipes at the C# language.

In this article I could have outlined my disagreements with Nick’s specific allegations about the Framework, or I could have talked about the sheer difficulty of writing a complex framework, or I could have explained how cross-language cultural issues make using a different framwork difficult. However, I decided instead to focus on one paragraph from Nick’s article:

"Maybe someday someone can explain to me why so many classes in the FCL are marked sealed. Shoot, why is it even possible to ‘seal’ a class. What the heck is that all about? Who are you to say I can’t improve or enhance your class? If your class somehow needs to be sealed, then I say you have a design problem. Now, despite the fact that most of your OOP languages include the ability to “seal” a class — C#, C++, Smalltalk — I am undaunted in my view. I was hoping that the FCL designers would be the ones to see the light and let me descend from the String class. Shoot, you can’t swing a dead cat in the FCL without hitting a sealed class that desperately needs enhancing.

Let’s focus in on the real issue: Should a modern object-oriented language allow classes to be sealed, and thereby bar subclassing from them?

The answer to this question involves a detour into designing libraries. The success of modern OO languages is due, in part, to the ability to use libraries for the development of large-scale systems. In an ideal world, those libraries should be secure, reusable, well-tested, and performant. In the real world, they sometimes miss the mark, but we can at least hope that they are reusable.

To be reusable, a modern OO library depends on the pillars of OOP: encapsulation, polymorphism, and inheritance. Since the development of Java, inheritance has been mostly replaced by delegation or composition. Indeed, way back in 1995, the Gang of Four said this: Favor object composition over class inheritance. (Page 20 of Design Patterns. It’s one of the two predicates on which the rest of the book depends.)

Encapsulation is an important principle for libraries since it enables the writer’s of the library to hide the functional implementation of their classes and methods. This in turn means that classes can guarantee that the data they hide can be only changed by methods of the class itself. If you use the Design by Contract pattern — and you should — then you will always be sure that the parameters to your methods are valid. But you only need to apply the contract to outward-facing methods. The inner private or protected methods don’t need to obey the contract because they are only called from code you control and own.

Since your code is the only code that can write to the class’ private fields you automatically make the class easier to test, make its behavior easier to predict and document, and make the methods easier to profile and optimize.

Another great benefit of encapsulation is a strong contract with the outside world: Here is this class and here’s the interface to it (defined as a set of methods, properties, and events). The class is a black box with certain well-defined knobs and switches on it. The maintenance programmer at the library vendor who has to fix/extend the class in some way has one of two possible avenues to explore (although they can overlap):

  1. An internal change to the implementation
  2. Or a change to the interface.

The first can be done almost with impunity so long as the published behavior doesn’t change (encapsulation means never having to say you’re sorry for an internal change). The second is a contract-breaker and the maintenance programmmer has two possible solutions: make the breaking change and suffer the slings and arrows, etc, or possibly write a new class altogether (the old "Ex" suffix solution). Both are nasty.

There is a great problem with using encapsulation though. That is inheritance, one of the other great principles of object-orientation (although as I mentioned above somewhat deprecated these days).

Consider this from the library writer’s point of view. You must write a base class that encapsulates some behavior and you want to make it extensible so that some unknown programmer in the future can subclass it in some unknown way. You know that encapsulation is good; however, you have a unique problem: you must break encapsulation in order to provide override points for the subclasser. You look surprised, perhaps. Yet, why otherwise have the protected keyword? The very existence of this keyword means that encapsulation is being broken, albeit for the limited use of someone who will be subclassing the base class (which in reality means everyone).

All of a sudden, this class no longer has this strong encapsulation contract with the rest of the world. You have to expose — to a certain extent — how you are implementing the class. A corollary is that you have to provide a weaker contract to the subclasser: I promise not to change the implementation of my class "too much", with some hand-wavy gesture.

But it doesn’t stop there. As soon as you expose part of the implementation of the class, you’ll be opening the door to someone who will say: you know, it’s nice that this class is subclassable, but I really need access to this little private field for my own derived class. Please? Pretty please?

Of course, another problem to solve is how to fit the extensibility points for polymorphism into your base class by marking some methods as virtual. (Java has the opposite problem: since all methods are virtual by default, which do you mark final? Or do you just ignore the issue?) Since virtual methods are known to be slower at calling (there’s a double redirection going on) you don’t usually want to go the whole hog and mark all protected/public methods as virtual. All that will do is to bring down the ire of the premature optimizer.

We used to wrestle with this constantly at TurboPower. For at least one product, we even went to the extent of having a compiler define that switched all private sections to protected ones, just because we didn’t know how to solve the "expose part but not all" inheritance problem. And I think we were fairly intelligent people. It’s just that the problem of designing a class hierarchy or framework that can efficiently be extended by third-party programmers is hard. And then you have to document it, hopefully well enough that those third-party developers can understand how to extend your base class.

There is another problem (another? you’re nuts: writing libraries is easy, dude) that, frankly, not many programmers appreciate or even care about. That is one of security. You see the whole point of polymorphism is that you can pass around objects that look like BenignBaseClass instances but are in fact HostileDerivedClass instances. Every time you implement a method in your library which takes an instance of BenignBaseClass, you must ensure that the method is robust in the face of potentially hostile instances of derived types. You cannot rely upon any invariants which you know to be true in BenignBaseClass, because some hostile hacker might have subclassed it, overridden the virtual methods to screw up your logic, and passed it in. Evil laughter.

Between a rock and a hard place, eh? In essence you just can’t have pure encapsulation and unrestricted inheritance. It just doesn’t work like that; never has done. Fooey to those pillars of old-style object-orientation, welcome to compositional object-orientation. The King is Dead, Long Live the King.

Don’t use inheritance unless you are writing a self-contained set of classes in your library or framework. I now use inheritance so infrequently that I always seem to have to reread the C# Programming Language book to understand how to call the base class’ constructors. Go with what the Gang of Four were saying 10 years ago (as Delphi 1 was just coming out): prefer composition over inheritance. Of course, for that your library or framework has to be designed around interfaces, and that takes some mental acuity or you won’t get the abstractions right. It’s not as hard as determining extensibility points of your base classes, but still challenging.

And since your library or framework users are modern OOP programmers, they understand the issues and welcome being able to use interfaces, and you can seal your classes, at least those that you determine should not be extended. Enforce encapsulation, it’s the strongest of the pillars. After all, in C# at least, if you get it wrong (and one of your users comes up with the canonical case for allowing inheritance to work), you just unseal the class. It’s a non-breaking change. (The reverse is not true: someone might have written a derived class.)

Admittedly there is a tradeoff here.  On the one hand you have the developers who want to save a little development time and effort by treating any old object as a "bag o’ fields" (if it has some methods, w00t, bonus!), and on the other hand you want to design and implement a fully-featured, robust, secure, predictable, testable library in a reasonable amount of time. The latter will certainly involve sealing classes that you don’t want developers subclassing for whatever reason.

Sealing classes is a perfectly valid thing to do. Throw away those awkward frameworks based on class inheritance. Move away from class inheritance to implementation inheritance. The grass is definitely greener over here.

No comments yet

Leave a Reply

You must be logged in to post a comment.