In my last blog post I talked about people pushing a process as the magic bullet when it is the people that matter. Having the wrong people on a team can cause havoc and kill a project. Sometimes these wrong people are cargo cult programmers.
According to Wikipedia: ‘Object-oriented programming (OOP) is a programming paradigm using “objects” – data structures consisting of data fields and methods together with their interactions – to design applications and computer programs.’
OOP is supposed to be a practical way to organize a program into hierarchies of objects where similar objects can inherit behavior from each other and override that behavior when necessary. Objects can also contain other objects and that is a technique called composition. Certain programmers pick up OOP and fall in love with the rule-set without fully understanding it, or they over-apply the principles. These people are cargo cult programmers.
I have met programmers who believe that anywhere there is a conditional statement in OO code, there is cause to subclass, “because that is the OO way! ™”. And they will defend it against any pragmatic reasoning. So anywhere you see an if/then/else or a switch statement, you should find a way to break the logic into seperate objects to avoid the logic. The dogma here is that conditional statements complicate things and are not strictly OO, so they must be minimized and preferable erased.
A Car and a Train and a Truck can all inherit behavior from a Vehicle object, adding their subtle differences. A Firetruck can inherit from the Truck object, and so on. Wait.. and so on? The thing about inheritance is that is so easy to create massive trees of objects. But what OO-bigots won’t tell you is that these trees will mess you up big time if you let them grow too deep, or grow for the wrong reasons.
Programming like this might not be a problem on a small to mid-sized one-man project, since there will be a limit to how much you will need to subclass to get a viable solution to whatever problem you are attacking. But on a 100KLOC+ sized project with thousands of classes, you get into big trouble. The project transforms from manageable inheritance trees and simple classes into an unmanageable mess, with stack traces so deep you need diving skills to reach the offending code. If you are really OOP obsessed and have been using interfaces to avoid being implementation-dependent, then you are in for a real treat. You will end up at the bottom of the stack trace looking at some offending code that clearly fails, but when backtracking to figure out how it got in this state all you encounter is interfaces. So you spend half the time finding out what implementation of said interface is being used and then find out that it is calling super.somemethod(..) which again calles super.somemethod(..) and so on all the way up the inheritance chain.
And then there is the issue of needing to change something in an object near the top of the inheritance stack, which in turn changes the behavior of the objects below in sometimes undefined ways. The deeper the inheritance tree, the worse things get when changing top-level objects. You can of course (and should) have unit tests and regression tests to ensure that the behavior remains the same, but these tests are just crutches that will help you dig yourself into a deeper hole.
The real problem here is that using inheritance too aggressively to simplify the internal logic of individual objects is a bad move. It’s using the KISS (Keep It Simple Stupid) principle, but in the worst way possible. You might think you are adhering to good OOP practice, and your classes might be simple and conditional-free, but no one (including you) will be able to change or debug your code in a painless manner, even with unit tests and regression tests.
When wanting to keep things simple you should try to let the code flow in a linear fashion. Any related logic code should be kept as close as possible, even if it means using conditional statements. It is a lot easier to read code if most of the important bits are in the same object or file instead of spread out between multiple classes. You should of course use inheritance, but moderately and when necessary. You should also abstract out logic into appropriately named methods to keep a readable level of abstraction. And you can also use composition to add behavior to your objects. You want to maximize readability because that is what matters later on when you are maintaining or debugging. That is the pragmatic approach.
No one is impressed by how OO your code is if it is impossible to debug.
Excellent advice.
I don’t think inheritance is a good composition mechanism and most design patterns basically are remedies for its shortcomings. Modern languages now have generics for parametric polymorphism and first class functions or close approximations which offer much better ways of structuring and composing code units than inheritance.
Inheritance is only a problem when it is overused to create deep inheritance hierarchies. I follow the advice given in the Gang of Four book which states that you should only ever inherit from abstract classes. Object composition is only for those who don’t understand how to use inheritance properly.
I think people forget in trying to simplify one area it can make other areas more complex.
An analogy would be the jigsaw puzzle – if you have small pieces they look simple because they have simple image on but you have a hugely complex puzzle to do.
Large pieces will look complex because they have more complex images on but it will be much simpler to put together.
The trick is to balance the two areas of complexity such as to give the best overall minimum level of complexity
You do make some salient points, but I’m having trouble really getting to the root of what you’re saying with this piece of your post:
“You can of course (and should) have unit tests and regression tests to ensure that the behavior remains the same, but these tests are just crutches that will help you dig yourself into a deeper hole.”
If these tests are just crutches, why should people use them? It seems like you’re advocating using unit tests, but that people should avoid using them for the only thing they’re good at — ensuring that the object’s interface behaves the way its contract says it should.
Jeff:
I see that paragraph is a bit unclear. I do advocate using unit tests.
The crutch part was referring to building deeper and deeper inheritance trees. Unit tests won’t save you from writing bad code, they will just make sure (hopefully) that the bad code still runs as intended.