In the not too distant past, I was tasked to research and try to make concrete a corporate CTO's vision of software engineers adopting DBC, which the CTO seemed to passionately believe in. The company uses Java as the primary platform, with a great deal of C++ (in reality C) in legacy code. Since most of the literature on DBC is in Eiffel, part of the task was to attempt to find implementations and approaches suitable for Java. I was part of a Java platform group, so I did not concern myself much with C and C++.
DBC is a little bit of a strange animal. Once you learn it and see its power, you become passionate about it. But adopting it and making it part of your daily practices is an altogether a different ball game - if your language, development environment, and production environment don't support DBC, as the case is with Eiffel. I will try to explain this in as few words as possible. I hope it will come through.
The essence of DBC is, of course, Design and Contracts - that much should be glaringly obvious from the D and the C in the acronym. Being able to write contracts for a class, or methods of a class, helps you design it. That is the main theme. You design by writing contracts. DBC is a design methodology. The contracts are not the end. They are the means to better design. More robust (more reliable and less error prone) design is really the goal. Higher quality software, in other words, is the ultimate goal.
The process of writing contracts is deceptively simple: for a method, specify the preconditions under which it is legal to call the method, specify the postconditions that must hold after the method returns, and specify conditions that must hold all the time for the class (invariants). This is much easier said than done - simply because programmers don't think that way. Mathematicians think that way. Programmers seek the goal of a working program - not a provably correct program. They hope to achieve correctness and reliability by demonstrating them through testing. Many of the goals of DBC can be achieved by testing. Testing is complementary, not contradictory, to the goals of DBC. It is just a different process and a different mental model.
An example should illustrate. Lets take the Stack class. We all have an intuitive feel for what a stack should do and what rules of behavior it must exhibit. We can have a mental model of a stack of dishes in a restaurant implemented via a spring-loaded mechanism inside a cylinder. A stack starts out empty. You push one dish on it, then a second dish, then a third, up to its capacity -say 20 dishes. Then the physical mechanism will not allow more dishes to be stacked on top. When you need a dish, only the one on top is physically reachable. So you reach and pop one dish off the top, the dish below it, pushed up by the spring, then becomes the top of the stack ready for another pop. Once the stack is empty, it is physically impossible to try to pop a dish. There aren't any available on top and this is obvious by inspection.
A software stack is similar, except since it is a conceptual construct first, and then a software construct second, we don't have available to us the physical things we take for granted with a real-life object. So an implementer of the stack has an intuitive feel for needing a push(), a pop(), and is-empty(), and is-full() operations. He also has the intuition that you cannot pop an empty stack and you cannot push onto a full stack. He implements the push and the pop, and tests anything that is likely to fail - and moves on to bigger things. Because, in the course of a programming day, there are many more tasks much more difficult, complex, and demanding than a stack.
Using DBC to specify, even before thinking about implementations, is where the challenge of DBC lies. We need a specification that defines contracts so that the specification is complete and allows the implementer to provide a robust implementation - without constraining the implementation.
The thought process for specifying a stack with DBC goes something like this: I need a push(), and pop() operations for the stack to be useful - that much is obvious from the needed functionality perspective.
Now we need some contracts. Some are obvious: lets say we have a count and a capacity attributes. A class invariant is that count >= 0 and count <= capacity. This holds for all instances, at all times. For push(), a precondition is that stack be not full (i.e. count < capacity). Simple enough. A precondition says that it is illegal to call push() when the the stack is full (at capacity). The word illegal means that the push operation will be prevented from being called. It will not ever be allowed to be called, with invalid preconditions. It should be quite clear that the push() method itself cannot be the one checking on the precondition, because if the precondition is false, it will not be called. We will get back to this point when we examine the issue of the sloganeering CTO a little further on. In the contract specification for push(), we simply note the pre-condition that stack must not be full - in the understanding that this is a directive to the environment not to allow a method to be called when its precondition is false. That is the meaning of "illegal".
Now we have to think about the post-condition. Writing post-conditions is hard because you must specify the future state of the class as a result of the execution of the method. That is not always easy to specify. It is at a level of difficulty equal to writing the implementation. That is why I think many Java programmers are unlikely to use DBC. For the push operation, the post-conditions are that the new top of the stack must contain the item just pushed, and the item that was previously at the top, must now be the item second from the top. You see now that the notions of "previously at" requires the examination of the state just prior to the execution of the method, and comparing it with the state just after the execution. The mechanism for specifying postconditions must have an expression language that has that notion. Eiffel has the "old" keyword for that purpose, which Java, of course, does not. Now the issue of making assertions about the position of the item below the current top gets into a discussion of how much about the "internals" of the stack do we expose. Debate is likely to flare up that we cannot be examining positions of items, because the stack model is concerned only with the item on top, or is it? This is what writing postconditions leads to - a healthy debate about the essence of the class - what is internal, what is visible. Again, I don't think many Java programmers will take to that kind of thinking. It will slow their coding down.
Now consider the post-conditions for the pop operation. The pop operation must return the item that was on top, and the item that was below it must become the current top. So the notion of what is currently on top, what previously was on top, and what was below top, all have to find expressions in the expression language and the queries on the state of the class. This is again easier done with tests than with writing pre-conditions. But now you are back to mixing specification with implementation, which Java programmers do all the time, as they circle through test code and implementation code. They "do" the contracts through tests, instead of specifying them. Two problems with that: 1) the tests are not part of the specification, so they are likely not to be read. There is no expectation that the tests constitute part of the specification. That is too much of an overloading of their function. 2). tests make assertions on implementations, so each implementation must have its own set of tests - which greatly increases the workload of writing assertions to assess quality and completeness of specifications. These are serious problems - serious but subtle and likely to be missed and dismissed.
My thoughts at the end of my research phase was to try to address all these difficulties in bringing DBC to the world of Java. As seen from the above discussion there were many issues, many of them at the heart of Java best practices. How will we get Java programmers to specify before they implement? How do we capture specifications so that the implementer is still free to choose the implementation? How do we implement the notion that "it is illegal" to call a method when its pre-condition is false? Clearly this is a role for the runtime environment - contract checking must be done in the environment, not in the code. But we cannot change the Java compiler (not yet). We can however, enhance the runtime environment, by, say AOP. We can add aspects that check contracts. We can fine tune contract checking by providing runtime attributes to the contract checking aspects. Allowing contracts to be specified in interfaces and checkable through aspects will provide the best of possible worlds for the Java developer. The expression language for writing contracts can be provided by simple annotations. There are several Java open source implementations that provide exactly that mechanism of enabling DBC in Java. The task was simply one of education and raising awareness about DBC as a best practice. We would have to show how DBC is complementary to other best practices, like Test First, or Test Driven.
Back to the sloganeering CTO. I had a session with our CTO, who was now sloganeering about "Outrageously Great Software", an obvious, and unabashed rip-off of Steve Jobs's "Insanely Great" software. I wanted to discuss how the two slogans are, in fact, almost one and the same. DBC allows us very high quality software that meets its stated specifications, and great software is software that meets its specifications, does not fail, and delights its customers. Very closely aligned goals. Half an hour into the meeting with the CTO, who has cultivated the image of being almost a god at the company, he proceeded to tell me that outrageously great was anything like the iPhone. I thought, oh, all we have to do is worship Steve Jobs here and our practices are blessed. Ok, not too bad. But I need a little bit more specificity and concreteness. We all know how wonderful iPhone is, but what exactly are the engineering practices that will lead us to that engineering nirvana? The CTO proceed to expound on DBC. All you have to do, he advised, is "assert out early". Huh? He proceeded to then show me a 10,000 line C file (which he asserted was C++). It had one function after the other, just checking its parameters for being zero or null. Essentially he had boiled down DBC to, "if not pre-condition, then error". That is it. All the issues that I went over about the design process for coming up with pre-conditions, post-conditions, and invariants are not addressed. The issue of how contract checking is done, and by whom, is simply disposed of by making each function just check its parameters, and "assert out early". Nothing about post-conditions, and the need to reason at least about the state the object is left in, and how it relates to the previous state before the execution of the method. The notion of a contract was reduced to function parameters being not null. The anticipated design process that would have lead to a healthy reasoning and debate about what features a class should have, what features it should expose, and why simply vanished. The greatest benefit of DBC, the design methodology, was dismissed for the benefit of "assert out early". Pretty damned stunning trivialization of a very promising design method - coming from a CTO. I was actually hoping to instill an "interface driven design" methodology, through DBC. Where engineers focused on interfaces and minimzed talk about implementation. This would have the benefit of improving communcation among all the stakeholders of a development effort. If everyone focused on interfaces with their clients, then developers can make businss clients focus on clarity of interfaces, i.e., better business requirements, testers would make their clients, the developers, focus on interfaces, and their tests. I.e. everyone would know his contracts, and everyone is clear on his client's expectations. There is much, much more to DBC than "assert out early"!
The stunning part about this experience for me, is that the CTO then reported to my management, who are under him, that "Nabil does not get it". To his mind, I did not get DBC, because I thought it was more than "assert out early", and it required more at design time, more at implementation time, and more at runtime. He also declared that "AOP was not ready for production use yet" at our company.
I hope none of you have similar experiences, or work for sloganeering
CTOs. My biggest sadness from this story, is that we were prevented
from achieving great software, by a leader who preached
great software, but never did articulate what you needed to do to achieve it - actually articulated a caricature of what was needed, and dismissed serious efforts at a meaningful translation of vapid sloagans into reality. Of course, he moved on to another company, where his slogans now sound fresh and inspiring. It is a lot easier to talk like Steve Jobs. It is much harder to achieve what Steve Jobs does achieve, through real inspiration of what his engineers do.
Comments