This book is a work in progress, comments are welcome to: johno(at)johno(dot)se

Back to index...

OOPs - nasty traps in Object Oriented Programming

"Luke, you will find that many of the truths that we cling to depend greatly on our own point of view..." - (the ghost of) Obi-Wan Kenobi

The thread of this book is Immediate Mode. For me, that's about being direct and explicit about things in your code. It's about avoiding extra stuff (and state), and in a lot of ways, depending of course on your point of view, may be seen as running contrary to the principles of Object Oriented Programming.

Being a programmer who was "raised on the stuff", you might find it strange that I am leery of OOP these days. Maybe it's just my inquisitive nature that eventually forced me to question the most basic foundations of everything I knew about programming. Maybe it's just that after a while I realized that I couldn't find two people's opinions of what OOP actually WAS that coincided.

Not to say that I don't use some of it (whatever IT is...), but with power comes the risk of blowing your whole leg off (yes, I know that has been said before). Let's just say that it's really really easy to use some feature of OOP (and of C++ in particular) and get a whole bunch of nasty side effects that you didn't really want to have. It's also very easy to try to make classes and objects of everything, even things that are intangible, and face it, sometimes that doesn't make any sense. It's also extremely easy to become so enamored of your great shining castle of abstraction, that you "abstract away" every real functional value that your program every might have aspired to have.

An Object Is State and Behaviour

Early on I was made to understand that an Object, being an instance of a Class, which is a Type, "Encapsulates State and Behaviour". That's quite a few concepts right there in that one sentence. The thing that bothers me about being purist about this, is that you aren't supposed to see the state. You see an object, and you can "send messages to it", and you can "ask it about it's state". Actually, I think you should say, to be really purist, that you can "ask it about domain-related state". That implies to me that primitive types like ints and floats are too low-level/general to expose.

Sure, ok, I'll go along with a lot of the above. But sometimes, I think state is just state. Just a number, just an index, just a factor. This is where the OOP way kind of breaks down, because it's really hard to do anything meaningful in a program without actual access to pure state.

For example, what are rows in a table in a relational database? Are they objects (i.e objects = rows of a table = class)? Well, I guess that a row in a table could represent the state of an object, but certainly not the behaviour of one. And look, it's all icky, because you see the basic types of the state, and those types are all generic and not domain specific, which means that you have to manually do a lot of range validation and the like.

And besides, you may argue, relational databases aren't Object Oriented, perhaps because of some of the aspects of the above. Well, ok, but I don't think anyone will argue that the relational model isn't at least "useful", so does that imply that OOP is not? Ok, I guess that's going too far, but I would have to say that pure state is definitely a real thing, and sometimes it is "just state", and so we should treat it as such. Like structs, for example.

In the end, maybe I and others are reading too much into this. I have read that Bjarne Stroustrup never evangelized classes as being something significantly different from structs, only that the features of a C++ class allows you to enforce the concept of an Invariant. I really think that this view is a very useful one. This is where all the "nasty manual range checking stuff" goes. The constructor sets up the premise and operating environment of an object, and the destructor cleans it up. In between these two, the features of a C++ class allow the program to enforce the invariant that the class conceptually represents. Very useful. But it doesn't mean that you can't have public ints that clients both read and write. That's something else. That's data. That's pure state.

Polymorphism or "Is You A Can Of Coke?"

" basically, the program spends all available cpu-cycles asking each and every object 'is you a can of coke?'..." - Valli Noghin

I think polymorphism is useful. I use it all the time when I want to have multiple implementations of a method, when I want clients to be truly independent of who the "message is actually being sent to". But then, I really mean it too. I don't want to know who is receiving the message in those cases.

I also think that polymorphism is one of the most misused features of C++, because more often than not, people use it for really insignificant aspects of the features it provides, while otherwise completely breaking their own backs trying to get around the rest.

One really common trend in games is that of the concept of an Entity. An Entity is most often a baseclass, it could be simply an interface or maybe have some state, but in any case it's the really high level concept of a "thing" in a game world. A truck. A tree. A player's avatar. A bullet. Whatever. Some "architect" decrees (with his design hat on) that there is enough commonality amongst all "things" in the scope of the game to motivate that they all inherit from/implement Entity. And guess what? That allows us to have them all in the same list! Only a single list! Or datastructure. Or octree. Erhm.

So far so good, we have all our Entities in one place. For each game tick, we update() them. They "do stuff". Great! But then wait, we want to do some kind of query, like from ExplosionEntity::update(), who wants to find all ExplodableEntities in its radius and explode() them. So we basically work something out where we can do a template based query in the big singular Entity list, passing the type of the Entity we want (ExplodableEntity) plus a radius, and getting back a temporary list of the ExplodableEntities that were in range. Then we iterate that and explode() them.

Well, that works. But step back for a minute. Why are we hiding the ExplodableEntities in a big list of Entities in the first place? We know that we want to do queries like this (explode things), it is a fundamental part of our game design. So why are we making it hard on ourselves? Why are we spending all that time figuring out the concrete class of objects (often using some custom and really optimized form of RTTI)? Why don't we just design the whole thing so that we can actually see the things we want to see the way we want to see them, i.e. by concrete class?

C++ allows you to express your design really concisely, and in this case, you want to be able to get at the ExplodableEntities and explode() them, so put in a list of ExplodableEntities somewhere. It is entirely possible to design the program based on the types of INTERACTIONS between different parts of the program, not based on the real or imagined implementation / type / concept similarities between objects. In my experience thinking about that, and designing around that, makes the program so much simpler to understand, maintain, and extend. And it really really does offset the "pain" of not being able to have everything in a single list. I mean, you can still have all your Entities in a single list to be able to update() them (that's a good thing, because in that case you really want to just update() them, and all of them at that), but you might want the ExplodableEntity list too.

Seriously, who are we hiding what from? Certainly we don't want to actually hide the concrete class of an object from ourselves, which we prove by having the template queries. If we want to know the concrete class of an object, we should express access to that object, by class, directly.

What About Software Re-Use?

Like polymorphism, the holy grail of OOP reuse is something that seems really great in concept, but it tends to shoot you in the foot. Indeed, thinking about reuse too much is even more dangerous than messing around with polymorphism.

Premature reuse is in my opinion even worse than premature optimization, often cited as "the root of all evil". Eek, what does that make premature reuse then? Becoming overly enamoured with the POTENTIAL for reuse gets you thinking about systems and frameworks, things that are general and flexible, as opposed to creating real value and functionality in your program, which are things that are specific and concrete.

In my early programming career, I though the whole OOP reuse thing was fantastic, and spent many hours thinking of ways to make things generic and reusable and pluggable. But eventually I realized that this didn't really help me implement any gameplay features. And the designers always wanted special cases anyway, things that I hadn't anticipated, and that made me really angry since they didn't fit into my grand design. But then, how truly generic and reusable was that design in the end?

Getting out of the reuse mindset helped me to finally get out of the "systems architect" mindset that I think is a big part of being an OOP programmer. This really happened due to the realities of the day to day development of Ground Control 2, which forced me for various reasons to give up on my grand designs and just go with the flow, adapting to changing requirements on a daily basis.

The funny thing about all of this is that the "pain" of having the designers mess with my "system" was quickly replaced with a joyful feeling of coding stuff that was "useful" and "to the point". I experienced a great resurgence of the sheer fun of programming, and I think that is very much due to me consciously letting go of my need to control the design of the software. Instead I could simply admit that I didn't and couldn't know everything beforehand, and let myself discover the way the software WANTED TO BE in the same instant I coded it. I firmly believe that Ground Control 2 is both better software and a better product as a result of this paradigm-shift.

which leads to the REFACTORING SECTION - They say that hindsight is 20-20, and a smart man said something very interesting about Refactoring. That it is in fact Analysis. And what better time and vantage point to to Analysis on software than when you've already coded it and can see directly how it turned out...

I'm not saying that you shouldn't reuse software. You should. But you should not plan for reuse. You should discover opportunities for reuse, often when you find yourself to be duplicating work you have already done, and when you do, you should extract the actual real commonality and put it somewhere reusable (i.e. in a common library). Truly reusable software elements are discovered, not designed.

Another take on Software Re-Use...

For many OOP practitioners, Reuse is like God. God is good. Reuse is good. God is holy. Reuse is also sometimes seen as holy. But many are the crimes that have been perpetrated in the name of God. And so it is with Reuse as well...

Again, my basic take on software reuse: Never design for reuse. That is speculative, and you are better off writing code that you know you need right now than code you think that you might need tomorrow (is this the same as YAGNI?). Only when, whilst maintaining software and writing new software, you realize that you are doing something that you've done before, only then do you reuse some code.

Even in this case you have options: if you are still maintaining the codebase from which you are reusing code, you factor out the commonality to a library and make both the old and the new software depend on this common library. If you are not maintaining the old software, you simply copy the source.

This view is based on a few things: We all want to write successful and useful software, otherwise what is the point of writing it in the first place? The natural state of such software, due to it's usefulness AND softness, is a state of constant maintainance. The reason to extract commonality is to have less code total to maintain, which makes maintainence easier. This gives rise to reuse of the common code.

Another reason to reuse code is simply because software is trivial to copy (if you have access to the source), so why duplicate work effort that you can simply copy for free? It's still reuse. It is a very common form of reuse, especially when it comes to copying/reusing an entire application. At the very least it will give you a much better startoff point for the new application than having nothing at all to start with. The decision to do this kind of application-wide reuse is based much on how similar the new application is to the old, because you don't want to end up rewriting/adjusting more old code than you write new code.

Reuse can happen on small scales too, even within the same application. This is typical when you are just starting on a piece of new software. In the natural course of implementing customer value, you realize that you can factor out some commonality or parameterize some method in order to minimize the total size of the codebase. This is called Refactoring, and a very essential and natural part of natural software growth and adjustment. I think reuse is part and parcel of the exact same mindset (as Refactoring), but people become confused due to the (often only perceived) difference in scale and scope.

Designing for reuse is more often than not based on a "coolness" factor, i.e. it "feels cool" to design and implement reusable systems. The fact of the matter is however that such design is ALWAYS speculative, because you are anticipating things that you MIGHT NEED, or COULD DO (this is the worst kind of speculation, because you are going outside of the scope of the requirements of the application at hand), with the system you are creating. That is not the point of writing software. The point of writing software is to deliver useful systems that deliver business value (or entertainment value, or both) to the end customer.

Back to index...