Niall makes the case for, but cautions about the drawbacks of, object-oriented programming. While it increases productivity, your software may end up being less robust.
The scene is a disused conference room. A team leader discusses the merits and drawbacks of object-oriented programming with a few of his programmers. As with many debates, sides have been taken before all of the points have been heard. We join the action as the argument heats up:
John Cleese: So what have the Objects ever done for us, eh?
Programmer1: Well there's code reuse.
John Cleese: Oh. Yeah, yeah, they did give us that, ah, that's true, yeah.
Programmer2: What about data abstraction?
John Cleese: Yeah, all right, I'll grant you the code reuse and data abstraction, the two things the Objects have done.
Programmer3: And inheritance.
John Cleese: Oh, yeah, obviously inheritance. I mean inheritance goes without saying, doesn't it? But apart from code reuse, data abstraction, and inheritance…
John Cleese: All right, but apart from code reuse, data abstraction, inheritance, encapsulation, and maintainability, what have the Objects ever done for us?
Programmer4: They brought polymorphism.
John Cleese: Poly who?
And so the debate rages on. While object-oriented programming has many benefits, the evidence tends to be anecdotal, rather than empirical. This does not mean that benefits do not exist, but we should not assume they are a given. The number of organizations with vested interests in selling various tools associated with OO languages and methodologies guarantees that plenty of literature boasting the benefits of OO will continue to proliferate. Such hype should always be questioned.
I am a big fan of OO methods. Much of the graphics work I do would be unbearably tedious without the benefits of inheritance and virtual functions available to me in C++. However, I think we need to determine precisely which areas of programming it benefits, rather than just assuming that it improves everything.
I will make the case that while OO programming increases productivity, it does so at the cost of diminishing the robustness of your software.
The holy grail of OO programming is code reuse. Yes, you can reuse code in non-OO languages, but OO languages allow you to tie code and data so closely that when you reuse code, the data on which it depends is automatically available. This makes reuse easier. But code reuse is not always a good thing.
A number of famous software failures have been related to software reuse. The Ariane 5 rocket crashed in June 1996. The root cause was the failure of an assertion in a piece of code that was performing an unnecessary calculation. Why was an unnecessary calculation made at all? Because the code that performed it was taken from Ariane 4, for which the calculation was, in fact, necessary. It was easier to leave in the obsolete calculations, rather than remove them, which would have involved changing code that they wanted to reuse as a complete block. Ariane 5 trajectories were not considered when the software for the Ariane 4 was written. The calculation exceeded its set of expected values, which ultimately led to the failure.
In another instance, air traffic control software written in the United States, and used there for many years, failed when it was reused in England. It could not properly handle crossing the line of zero degrees longitude.
The common thread of these incidents is that reuse is problematic when it takes place in a new context. Embedded engineers often have access to software that works in a desktop environment, but if it is not reentrant, the same code may fail miserably, and dangerously, on a multi-tasking embedded system.
The danger with reuse is that it is often not re-tested when it is reused in an environment with different requirements. That it worked quite happily for years on the old system might seem to imply that it should work well on a new system. New code would be designed with the exact requirements of the new system in mind, thereby avoiding the pitfalls I've just described.
Rewriting the code takes time and effort. The basic trade-off here is that the programmer is less productive because he is writing something that he might have taken from elsewhere. If the programmer's time is the only valuable commodity here, reuse is the way to go. But if you want your rocket to stay in the air, the programmer's time should not be considered the number one priority.
It could be argued that many of these problems can be avoided if the original programmer writes his program with the expectation that it will be reused. The implementation would have to be more general, and tested against more general requirements. The software would then be more likely to survive a change of context. However, you have to consider the time and effort spent making the code more general. These two expenditures may compromise the testing done for the original system. More often, code is designed to fit the general case, but tested only against the specific scenario. The result is a module that advertises a large range of functionality, when, in fact, only a small part of that range has been tested.
Encapsulating code and data means hiding it from the rest of the program. This allows other programmers to use classes without having to concern themselves with the internals of that particular class. The main advantage here is that the implementation of the class can be changed without having to alter the parts of the program that use that class. This gives us flexibility and makes it easy to change things. Last minute alterations are too easy, and sometimes we may pay the price. Very flexible and easy-to-modify software is generally in the programmer's best interest-but not in everyone's best interest.
The first fly-by-wire auto-pilot system applied strict rules to its programmers. The system ran as a single loop, which was not allowed to branch, call subroutines, or have any internal loops. All data was global. Ahhh, I hear the programmers scream-that would be a nightmare. It was, for the programmers. The testers loved it. The complete state of the system could be represented by the current program counter and the values of the global data. This made it straightforward for the testers to construct test scenarios, perform fault insertion, and then examine the response of the system. By taking all of the flexibility away from the programmers, the testers had a far easier job. The result was a system that was in use for 15 years without a single in-flight incident.
Flexibility would have made the programmers more productive, and possibly many extra features could have gone into the system, but this project made robustness the first priority.
Inheritance as an approximation
One of the fundamental building blocks of code reuse in object-oriented languages is inheritance. It allows the programmer to take a group of similar types and refer to them in a general way. In a graphical application, I may have Rectangles, Lines, and Circles. If they all inherit from Shape, I can write many of my routines to manipulate Shapes. The items on the screen can be represented by a list of Shapes. While the program is manipulating the list, it does not have to concern itself with the exact type of each individual object. The first item on the list may be a Rectangle and the second may be a Circle, but the list manipulation code sees both as Shapes. A routine to set the position of a Shape could change its x-y position without knowing the exact type of object. In this way, one routine can be used for all types inheriting from Shape, rather than having to write a new version of the routine for every different type. Hence, the routine is reusable for different types.
The benefit of inheritance is approximation. A Rectangle is approximately equal to a Circle, in the sense that they both have position and color information. The set of all of the approximately equal types is represented by the type Shape. The approximation is what allows us to be more general and, therefore, more productive in our code.
I see this as analogous to approximations in scientific theory. If I assume that a physical object interacts with the Earth according to Newton's laws of motion, I will have a very useful approximation of its behavior. For example, Newton's laws can tell me a lot about the movement of the Moon around the Earth. However, at some point, the laws break down. For some objects moving through the atmosphere, wind resistance will start to have an effect that needs to be accounted for, and this increases the complexity of the calculations. It takes good scientific judgement to know how far an approximation can be stretched without compromising the results.
With software objects, using inheritance as an approximation also means that we are sometimes going to get the wrong answer. Our Shape object may store width and height information. We can calculate the area of a Shape by multiplying width by height. For the Rectangle this will give an exact answer. But for the circle, the answer will be less precise. Many graphical applications accept this approximation. For example, a user might click on a point just outside a circle, but the application might select the circle because of this approximation. The software engineer has to use careful judgement to know when he will allow an approximation, and when he has to override the generic behavior to get more precise answers.
The problem arises because of this simple trade-off. The more often a programmer can use the generic behavior in the parent, the more productive the programmer will be. He will get more features into the product, but more cases of wind resistance will arise within the program. If the programmer now creates a new type, say Triangle, he already has a generic way of calculating its area. The language provides a mechanism for the programmer to specialize the behavior of Triangle to get a more accurate result, but often the programmer will not do that. Face it: we are a lazy bunch.
It could be argued that what I am describing is incorrect use of objects, but it is typical of designs I see all the time. Programmers will treat many different objects the same for productivity reasons, and later get caught out by the subtle differences. I am not saying you should avoid inheritance for this reason, but if you do use it, you may later find that your program contains more “approximations” than you would like.
The high cost of programming skills leads managers to choose a route that allows them to get more features-per-month (a more useful metric than lines-per-day in this context) from those programmers. However, putting productivity ahead of robustness and safety has an inevitable cost. It is a bit like the earlier days of the automotive industry when the top speed of a car was a major selling point. Making cars faster was considered progress, but it did not make them safer.
Am I suggesting that object-oriented methods are unsuitable for mission-critical systems? Not really. What I am saying is that the techniques were invented with the goal of reducing design complexity, but, in exchange, we increase language complexity. The more advanced features of C++ involve complexities that I believe are rarely justified in an embedded system.
A major factor in the design of C++ was the desire to provide features that allowed library vendors to package software for diverse customers working on different projects. The library vendors would sell a set of classes and templates that the customers would instantiate when building their own application. A library writer does not know that much about the customer's application, possibly because the customer and his application do not exist yet. The library writer might not know what type of number the user will want to use for his math functions. The solution is to provide templates. This allows the library user to decide later to use an int, float, or double, or maybe another type with overloaded numeric operations that the library user could create himself.
Another gap in the library vendor's knowledge of the client is the error-recovery mechanism. Some customers will consider a numeric overflow to be a disaster, immediately leading to a system reset and congressional inquiry. Others will have a backup plan, such as using a default value if a calculation fails, allowing the software to keep running. The library vendor cannot make this decision for the customer, because the correct decision for one customer will be the wrong one for another. The problem is addressed with exception handlers. The handlers allow fault detection to live in the library code, but the fault handling and recovery resides in the customer's code.
In short, we have added two features to the language that make it possible to sell libraries. The use of third-party libraries in C++ never reached its full potential in the desktop community, though there are notable successes, such as the Standard Template Library and a number of graphical and communications libraries. In the embedded world the use of such libraries is less common. Without the benefits of using third-party code, the complexity that exceptions and templates add to the language is rarely justified.
Multiple inheritance is another feature of C++ that adds enough complexity to tie the human mind in knots. It is occasionally unavoidable because you may need to inherit from a class provided by one library vendor and simultaneously inherit from a class provided by another vendor. This possibility is the reason a multiple inheritance feature was included in C++. Run-time type identification became necessary once inheritance trees included the complexity of multiple inheritance.
We now have a list of the most complex features in C++, the greatest benefits of which disappear if we are not reusing third-party libraries. The C++ language can be simplified by dropping these features. The Embedded C++ standard describes such a subset.
Many developers scoff at this standard, claiming that the more advanced features are useful and do not make the language overly complex. Tom Cargill once gave the C++ community food for thought with his paper, “Exception Handling: A False Sense of Security.” It challenged the best minds in the C++ world to make a certain short piece of reusable code safe from any memory leaks. The code implemented a stack that could hold any type of data. The type could be indicated as a template parameter by the application program using the stack. The code also raised exceptions in cases such as popping an item from an empty stack. The best minds rose to the challenge and solved the problem-three years later. Can you afford to wait three years to resolve all of your resource leak problems? Will you have the sharpest C++ programmers on the planet at your disposal to debate the trickier issues. I think not.
So objects can do a lot for us, but be careful not to buy into any more language complexity than you need. If you do find yourself fighting to control a project that has overused every feature available in the language, then console yourself with the promise that the “geek shall inherit the earth.”
When he is not watching old Monty Python videos, Niall Murphy writes software for user interfaces and medical systems. He is the author of Front Panel: Designing Software for Embedded User Interfaces (R&D Books, 1998). Murphy's training and consulting business is based in Galway, Ireland. He welcomes contact and can be reached at . Reader feedback to this column can be found at .
3. Leveson, Nancy. Safeware: System Safety and Computers. Reading, MA: Addison-Wesley, 1995, p. 30.
4. Ibid, p. 407.
6. Cargill, Tom. “Exception Handling: A False Sense of Security,” C++ Report, vol. 6, no. 9, November–December 1994.
7. Sutter, Herb. Exceptional C++: 47 Engineering Puzzles, Programming Problems, and Solutions. Reading, MA: Addison-Wesley, 1999.