Or, Throw 'em Away Until You Get It Right

Just before classes started, one of my undergrad research students stopped by to talk about a choice he was weighing. He is writing a tool that takes as input a Photoshop document and produces as output a faithful, understandable HTML rendition of the design and content. He has written a lot of code in Python over the last few months, using an existing library of tools for working with Photoshop docs. Now that he understands the problem well and has figured out what a good solution looks like, he is dissatisfied with his existing code. He thinks he can create a better, simpler solution by writing his own Photoshop-processing tools.

The choice is: refactor the code incrementally until he has replaced all the library code, or start from scratch?

My student's dilemma took me back over twenty years, to a time when I faced the same choice, when I too was first learning my craft.

I was one of my doctoral advisor's first graduate students. In AI, this means that we had a lot of infrastructure to build in support of the research we all wanted to do. We worked in knowledge-based systems, and in addition to doing research in the lab we also wanted to deliver working systems to our collaborators on and off campus. Building tools for regular people meant having reasonable interfaces, preferably with GUI capabilities. This created an extra problem, because our collaborators used PCs, Macs, and Unix workstations. My advisor was a Mac guy. At the time, I was a Unix man in the process of coming to love Macs. In that era, there was no Java and precious few tools for writing cross-platform GUIs.

Our lab spent a couple of years chasing the ideal solution: a way to write a program and run it on any platform, giving users the same experience on all three. The lab had settled more or less on PCL, a portable Common Lisp. It wasn't ideal, but we grad students -- who were spending a lot of time implementing libraries and frameworks -- were ready to stop building infrastructure and start working full-time on our own code.

Then my advisor discovered Smalltalk.

The language included graphics classes in its base image, which offered the promise of write-once-run-anywhere apps for clients. And it was object-oriented, which matched the conceptual model of the software we wanted to build to a T. I had just spent several months trying to master CLOS, Common Lisp's powerful object system, but Smalltalk looked like just what we wanted. So we made the move -- and told advisor that this would be the last move. Smalltalk would be our home.

I learned the basics of the language by working through every tutorial I could my hands on, first Digitalk Smalltalk then ObjectWorks. They were pretty good. Then I wrote some toy programs of my own, to show I was ready to create on my own.

So I started writing my first Smalltalk system: a parser and interpreter for a domain-specific AI language with a built-in inference engine, a simple graphical and table-driven tool for writing programs in these languages, and graphical front end for running systems.

There was a lot going on there, at least for a green graduate student only recently called up to the big leagues. But I had done my homework. I was ready. I was sure of it. I was cocky.

I crashed.

It turn out that I didn't really understand the role that data should play in an OO program. My program soon became a tangle of data dependencies design before I understood my solution all that well, and the tangle made the code increasing turgid.

So I threw it away, rm -r 'ed it into nothingness. I started from scratch, sure that Version 2 would be perfect.

I crashed again.

The program was better this time around, but it turns out that I didn't really understand how objects should interact in a large OO program. My program soon became a tangle of objects and wrappers and adapters, whose behavior I could not follow in even the simplest scenarios. The tangle made the code increasing murky.

So I threw it away -- rm -r again -- and started from scratch. Surely Version 3, based on several weeks of coding and learning, would be just what we wanted.

I crashed yet again. This time, the landing was more gentle, because I really was making progress. But as I coded my third system, I began to see ways to structure the program that would make the code easier to grow as I added features, and easier to change as I got better at design. I was just beginning to glimpse the Land of Design Patterns. But I always seemed to learn each lesson one day too late to use it.

My program was moving forward, creakily, but I just knew it could be better. I did not like the idea of maintaining this code for several years, as we modified apps fielded with our collaborators and as we used it as part of the foundation for the lab's vision.

So I threw it away and wrote my system from scratch one last time. The result was not a perfect program, but one that I could live with and be proud of. It only took me four major iterations and several months of programming.

Looking back, I faced the same decision my student faced recently with his system. Refactor or start over? He has the advantage of having written a better first program than I had, yet he made the sound decision to rewrite.

Sometimes, refactoring really is the better approach. You can keep system running while slowly corralling data dependencies, spurious object interactions, and suboptimal design. Had I been a more experienced programmer, I may well have chosen to refactor from Version 3 to Version 4 of my program. But I wasn't. Besides, I had neither a suite of unit tests nor access to automated refactoring tools. Refactoring without either of these makes the process scarier and more dangerous than it needs to be.

Maybe refactoring is the better approach most or all of the time. I've read all about how the Great Rewrite is one of those Things You Should Never Do.

But then, there is an axiom from Turing Award winner Fred Brooks that applies better to my circumstance of writing the initial implementation of a program: "... plan to throw one away; you will, anyhow". I find Brooks's advice most useful when I am learning a lot while I am programmer. For me, that is one context, at least, in which starting from scratch is a big win: when my understanding is changing rapidly, whether of domain, problem, or tools. In those cases, I am usually incapable of refactoring fast enough to keep up with my learning. Starting over offers a faster path to a better program than refactoring.

On that first big Smalltalk project of mine, I was learning so much, so fast. Smalltalk was teaching me object-oriented programming, through my trial and error and through my experience with the rest of the Smalltalk class hierarchy. I had never written a language interpreter or other system of such scale before, and I was learning lessons about modularity and language processing. I was eager to build a new and improved system as quickly as I could.

In such cases, there is nothing like the sound of OS X's shredder. Freedom. No artificial constraints from what has suddenly become legacy code. No limits from my past ignorance. A fresh start. New energy!

This is something we programmers can learn from the experience of other writers, if we are willing. In Unless It Moves the Human Heart: The Craft and Art of Writing, Roger Rosenblatt tells us that Edgar Doctorow ...

... had written 150 pages of The Book of Daniel before he'd realized he had chosen the wrong way to tell the story. ... So one morning Edgar tossed out the 150 pages and started all over.... I wanted the class to understand that Edgar was happy to start from scratch, because he had picked the wrong door the first time.

Sometimes, the best thing a programmer can admit is that he or she has picked the wrong door and walked down the wrong path.

But Brooks warns of a danger programmer's face on second efforts: the Second System Effect. As Scott Rosenberg writes in Code Reads #1: The Mythical Man-Month:

Brooks noted that an engineer or team will often make all the compromises necessary to ship their first product, then founder on the second. Throughout project number one, they stored up pet features and extras that they couldn't squeeze into the original product, and told themselves, "We'll do it right next time." This "second system" is "the most dangerous a man ever designs."

I never shipped the first version of my program, so perhaps I eluded this pitfall out of luck. Still, I was cocky when I wrote Version 1, and then I was cocky when I wrote Version 2. But both versions humbled me, humbled me hard. I was a good programmer, maybe, but I wasn't good enough. I had a lot to learn, and I wanted to learn it all.

So it was relatively easy to start over on #3 and #4. I was learning, and I had the luxury of time. Ah, to be a grad student again!

In the end, I wrote a program that I could release to users and colleagues with pride. Along the way, Smalltalk taught me a lot about OOP, and writing the program taught me a lot about expert system shells. It was time well spent.