Kent Beck is a consultant with Daedalos Consulting, Switzerland. He can be contacted at [email protected].
"In 1914 the nations of Europe were bound so tightly in bonds of trade and culture that war was simply unthinkable. There was only one problem with this theory."
"What was that?"
"It was complete bollocks."
Black Adder speaking to Baldric
THE OLD WORLD I can keenly remember sitting in a software engineering class as a junior in college. The professor was talking about the cost of change. He asked, "How does the cost of changing software change over time? If you catch a mistake, how much does it cost to fix it?"
And then he wrote:
Requirements: $1
Analysis: $10
Design: $100
Implementation: $1,000
Testing: $10,000
Production: $100,000
My eyes got wide. $100,000? I was never going to let a problem slip through that far. I would do anything necessary to be sure that I made all my big decisions as soon as possible. I didn't want to risk one of those $100,000 mistakes.
This is a rational reaction to an exponential change-cost curve. You want to make all of your biggest decisions in the project as soon as possible. If you decide right, you don't have to pay the big bucks. If you decide wrong, at least you tried.
THE NEW WORLD
Finding the Problem At 5 p.m. one evening while working on a life insurance contract management system, I got the crazy idea that the design of our transactions was wrong. Not just the design, but the original requirements were wrong.
When you implement a double-entry bookkeeping system, one of the options that you have is to split the credits or debits into pieces. Instead of having each leg of the transaction attached to one account, you can have each leg attached to many accounts. You can use this flexibility to create finer-grained models of the business.
Our system had this flexibility. But I never recalled seeing a place where it was used. As far as I could remember, the collections held by the "credit" and "debit" instance variables only contained one element. It would be an easy mistake to miss, in the crush of supporting production and enhancing the system as quickly as we could.
In most projects I have worked on, this would have been idle speculation. We made a mistake three years ago. Too bad, so sad. Back to work. But this wasn't most projects. This was a project that was using most of the set of practices I call "Extreme Programming," or XP. One of the core values of XP is aggressivenessif you see something wrong, you fix it.
Five minutes later, with an ad hoc query on our database, I had demonstrated that all of the transactions in the system indeed had only one credit and one debit. The code looked like this:
| complex |
complex := CSLife root allTransactions reject: [:each | each credits size
= 1 & (each debits size = 1)].
complex size = 0
I was using Gemstone/S, a Smalltalk-based object database. The query language is simply Smalltalk, and the elements in the database are Smalltalk objects. Writing little queries is as simple (or as hard, depending on how you look at it) as writing Smalltalk.
Fixing the Code It was now 5:10.
I called over my partner for the day, Massimo Arnoldi, and explained the situation to him. Could we fix the problem?
We talked seriously about the change. It would involve modifying the schema for all 300,000 transactions we had collected in the past year. We would have to be careful.
Our strategy was to take the problem in steps. First we would change the implementation of Transaction without affecting its external protocol. This would allow us to migrate the objects in the database while changing as little as possible in the system. Once we had the system stabilized, we would simplify the external protocol and change all the senders.
The external protocol of Transaction included messages for initializing the debits and credits:
debits: aCollection
debits := aCollection
credits: aCollection
credits := aCollection
and messages for accessing the instance variables:debits
^debits
credits
^credits
If we could preserve this interface while changing the implementation from pointing to a collection of TransactionsComponents to pointing to a single TransactionComponent, we would be set. When we set the value, what if we took the first (and only, we had proven) value out of the collection? Could it be this easy?debits: aCollection
debits := aCollection first
credits: aCollection
credits := aCollection first
Then the accessors could recreate a one-element collection before they returned their value:debits
^Array with: debits
credits
^Array with: credits
Creating these temporary collections all the time might be a little slower, but we had no proof that it wouldn't work.
It didn't seem like it could be that easy. We pushed the button that started our suite of 1,000 unit and functional tests. Ten minutes later we had a little more confidenceall the tests ran, and they didn't seem any slower.
Fixing the Data It was 5:30. The users were all logged off and the nightly batches were running. We had to figure out how to swizzle the objects in memory to reflect our new design. Gemstone keeps various versions of a class. When you change a class in a way that changes its structure, you have the opportunity to update all the instances. We made a practice of always updating all the objects, so we had only a single version of a class with instances at any one time.
The simplest form of migration:
- creates a new object,
- copies all the instance variables from the old object to the new,
- sends the message postMigrate to the new object, and
- changes all pointers to the old object into pointers to the new object.
We wrote migration code two or three times a week, so we weren't scared of it, but this was Transaction we were modifying. We wrote:postMigrate
debits := debits first.
credits := credits first
We couldn't figure out why this wouldn't work. We ran the database backup because the batches were finished; then we ran the migration code. It took about 10 minutes to run. It was now 5:45.
We couldn't believe that what we had done could possibly be so easy, but we also couldn't see any reason it wouldn't work. We had our shared experience saying it would work. We had our tests saying it would work. What else was there to do? We committed the changes and went home.
The Aftermath Before we had such an extensive test suite, there was always a moment of trepidation the first time we opened the error log. We had users who came in early in the morning. By the time we arrived, if we had created problems the day before, we would have a stack of error messages.
As our test suite grew, however, the drama leaked out of the unveiling of the error log. Nearly the only time we had problems was when we changed the rest of the systeminstalled a new version of Word, for example. From time to time we discovered there were things we weren't testing for in our code, of course, but the surprises were becoming rarer.
Figure 1. Design over time, old style.
This morning was no different. The error log was clean. We had fixed a fundamental requirements error and had done it in 45 minutes.
THE CURVE GOES FLAT If we had caught this error two and a half years before, when they were first developing the system (I wasn't there then), it would have been a matter of five minutes to fix. Here, two and a half years and 300,000 transactions later, the time had only gone up by a factor of 10. Where had that tyrannical 10n gone?
It was no accident that we were able to escape the clutches of the dreaded exponent. Here are what I consider to be the important factors in beating down the cost of change:
- Attitude: Most importantly, as a team we had decided that we would continually evolve our design. As we learned, we worked hard to make sure that our insights were reflected in the code. We wouldn't run from change; we would embrace it. Change would be our daily diet.
- Tests: Without the tests we would simply die. But tests can only prove the presence of features, not the absence of defects. However, there is some level of testing that is good enough so that you can act like you don't have defects.
- Pair programming: When making sweeping changes like changing the format of Transactions, nothing is more comforting than a second pair of eyes. We practiced programming two to a machine for much of our new production coding and all of our big transformations.
- Refactoring: We were used to making semantics-preserving transformations of our code and data. By discussing the changes with our pair partner, we became good at making those changes safely and quickly.
- Piecemeal growth: We had adopted the philosophy that we would change things a little at a time, then listen to the feedback we received. As long as we migrated a few database schemas several times a week, we never felt compelled to take big leaps. We could make a little change today, another tomorrow, and on and on. This kept the risk of any given change low.
- Technology: I cannot deny that technology played a role in the previous scenario. Smalltalk as a programming language and environment, particularly with the Refactoring Browser, is a fabulous place to make changes. The Gemstone/S database likewise encourages continual change. Objects tend to isolate change as a way of organizing logic, preventing infinite ripple effects.
THE FRIGHTENING IMPLICATION We'll get to your question, "What about us C++'ers?" in a moment. First, imagine if you will that your change-cost curve collapsed. What would you do then?
The rational response to a slowly rising change-cost curve (I don't know if it's logarithmic or assymptotic, but I know it rises very slowly after a time) is to defer decisions until you have to make them. The bigger the decision, the longer you want to defer it.
Conventional OO wisdom tells us to plan for future flexibility. If you need a little design today, better to put in a little extra for what might be true tomorrow. (See Fig. 1.)
A flattened change-cost curve turns this conventional wisdom on its head. Instead of putting in a little extra today, you wait until you need it, and then you put it in. (See Fig. 2.)
Deferring design has a number of advantages, short and long term:
- You get done faster today.
- You have less design to drag around between now and when you need the extra design. There is less to test, explain, document, refactor, and compile.
- You will do a better job of the extra design when you actually need the design. You will be able to apply all of your learning between now and then. You will know more about your requirements then, too, as well as more about the system.
- Tomorrow may never come. I review lots of code, and I consistently see features added that are never used. If you design on speculation, you may pay a cost in time, effort, inertia, and lost opportunity to no benefit.
WHAT ABOUT YOU? Objects alone do not guarantee a flattened change-cost curve. Any language/technology can be used to create an incomprehensible pile of, er, spaghetti (not to pick on pasta). But objects provide a natural encapsulation that tends to limit the scope of changes.
Here are some of the challenges in keeping software soft (to use Martin Fowler's phrase). Because I don't live in C++, you'll have to tell me if these are real problems, if there are others, and if there are things you can do to minimize their impact:
- Compile and link time. If it takes minutes or hours to test a change, you get much less feedback about your software. The ability to freely experiment with ideas is the key to learning fast.
- Support for refactoring. As Bill Opdyke's thesis on refactoring showed,/ it is difficult to restructure a C++ program in ways that probably don't change the meaning of the program.
- Support for bigger refactorings. A good configuration management system is vital to producing decent software. Unfortunately, the configuration management systems I hear about are optimized for working with a given directory and file structure. Moving files around and renaming classes can be difficult.
On the other hand, C++ has strong features like templates that can reduce the cost of change. If you want to work in the deferred decision style advocated by XP, you will have to find ways to reduce the costs imposed by C++ and its tools, and take advantage of its strengths.
I have found a few other practices that also contribute to keeping the cost of change down:
- Design simply. This is the absolute opposite of advice you'll hear most other places"Design for tomorrow"; "Put in hooks for future change." If I do the least I can to have a clean system today, then change is easier tomorrow. Most importantly, tomorrow when I discover an even simpler way to do everything, I have a chance to take out the extra complexity that has crept in.
- Anybody can change anything, anywhere in the system. Strict code ownership is good for preventing harmful changes to the system. But I see it preventing good changes to the system too. So the rule I teach is that if you see a change that makes the system better, you make it right on the spot (if your partner agrees and the tests still run). You need powerful configuration management tools to make this work, and you have to integrate every few hours, but the increase in the rate at which the system improves is worth it.
Figure 2. Deferring design until you need it.
CONCLUSION The collapse in the change-cost curve is the basis for a methodology called Extreme Programming. I am working on it with a few friendsWard Cunningham, Martin Fowler, Ken Auer, and Ron Jeffries. So far it has been used on small and medium-sized business-oriented projects. It may or may not be suitable for anything else. However, from my perspective it has been very successful, although it is clear that changing to XP is very difficult for some folks.
If you are interested in learning more, at the moment you'll have to turn to the Web. http://www.armaties.com presents many of the core practices. http://c2.com/cgi/wiki?ExtremeProgrammingRoadmap has a (somewhat chaotic) ongoing discussion of XP. Later this year Addison-Wesley will publish the first two XP booksExtreme Programming: Embracing Change, which is my XP manifesto, and Extreme Programming: Playing to Win, which is a more practically oriented presentation of the XP practices for project planning and testing.
I have to say a word about the opening quote before I finish. I am neither saying that the exponential change-cost curve doesn't exist, nor that the thinking that follows logically from it is wrong. I am saying that under certain conditions, the curve can be flattened, and if it is fascinating new things are possible. I am working to find more and more ways to make the change-cost curve flatter and longer, and exploring the implications. I hope you will too.
Reference
1. Opdyke, W. Refactoring Object-Oriented Frameworks, Ph.D. thesis, University of Illinois at Urbana-Champaign, June 1992.
Quantity reprints of this article can be purchased by phone: 717.560.2001, ext.39 or by email: [email protected].