So, how do you code?
published: Mon, 22-Sep-2003 | updated: Thu, 27-Oct-2005
It's a simple enough question, I suppose: "how do you code?" but it hides a plethora of axioms and assumptions.
Code where? Writing what? Who for? What standards should I follow? Coding from scratch or from someone's spec? Etc.
So let me tell you a story. Ever since I have been programming on PCs, I've kept my code. I copy it from old machine to new machine, I browse it every now and then looking for answers to questions. Last year I had occasion to reread some of it again. The reason? I had to map out a way of writing software that would enable the company I worked for to successfully rewrite a system, a system that had grown old, unmaintainable, poorly tested, irretrievably client/server, not very well written in VB6 (imagine seeing code that had seven On Error statements dotted through a procedure all going to the same place: the programmer just wanted to make sure, eh?). THe new system was to be multitier, web aware, much better tested, and written with C# and .NET. Several sudden changes: OOP vs. procedural, two tier vs. multitier, abstractions vs. knowing everything all the time.
A tall order? Maybe so. But I went back to look at my old DOS code and thought about how I coded then and the path I'd taken to how I coded now. I wanted to see if I could gather together enough ideas to help my crew of VBers get over the hump. Several things leapt out at me.
1. Unit testing. I reviewed some code I wrote back in 1990 for Turbo Pascal 6.0 that did basic date arithmetic (you know, date from year, month, day; date to year, month, day; subtract two dates; convert a date to a string representation; yadda yadda). It was all written in 16-bit assembly. I was kind of impressed that I remembered so much of it (the assembly language, that is, not the code). I also found the test program I'd written: it tested the date conversion routines for all of the dates in the 128-year period my date type covered. So: unit testing was very important to me then, and it still is now. I go about testing in a different way these days (test-first rather than test-afterwards), but I still need the boost fully functioning code gives me.
2. Abstraction. The date type I'd used was a synonym for a 32-bit integer (longint in TP). Although the language supported OOP, I decided to use an opaque type and to code manipulations on it in assembly for speed (and it was fast). The nice thing was that, because I'd written it in assembly, I'd abstracted the type's "methods". It required a definite shift of thought to read the code, so I never did once it was written and I was using it in applications. Nowadays, I'd probably use a date class (much as the .NET Framework does). To gain the same feeling that the abstraction is a good one, I would rely on the tests. So: abstraction is more than classes and interfaces, it's also about confidence that the code already written is doing its job properly and that it provides the right kinds of methods and procedures you need.
(Incidentally, this leads on to one of the important things that Microsoft ought to do: release the source code to the .NET Framework. I was able to gain a sense of trust in the Delphi code base because Borland released source code to the RTL and VCL libraries. I never quite got that with the Framework until recently, when I was able to peruse whichever parts of it I wanted). I could have written a series of tests to gain that confidence, I suppose, but never got round to it.)
3. Source code generation. The app I was working on in 1990 was a swaps trading system for a merchant banking division of Deutsche Bank. It used B-Tree Filer, a ISAM library written in Turbo Pascal. The system I was writing had quite a few tables: currencies, rates, futures, yield curves, holiday dates per currency, etc, etc. I was impressed to find that the junior Bucknall had written a program that would (a) generate other programs that would create the tables and indexes from Pascal templates, and (2) run said programs. In other words, using the computer to do the repetitive tasks quickly and accurately. Last year, I'd just been promoted out of TurboPower and there wasn't much call for writing such generator code, but in my new company? I recognized that in a fairly typical database-driven application or system, the vast majority of code can be auto-generated.
This is an important point that I feel I communicated well at my previous job. I forget how many tables they had--400 seems to ring a bell--and how many stored procedures (2000?), etc, but I made a forceful argument that (1) if you spend some time writing some hand-crafted code that creates tables, adds, updates, deletes records, and throw in a few other operations, and (2) make sure it's right by testing the bejaysus out of it, you can (3) write a code generator that reads some language-neutral table/record definition files and generates the rest of the code for the other tables based on those hand-crafter templates. By that I mean, generate the stored procedures to create the tables, stored procs to insert, modify, and delete records, a data access layer that converts and abstracts the database access layer into an interface or a class model (although there is more to this than meets the eye, a basic abstraction layer is still important and a good thing to have), the unit tests to verify that it all works, etc. If you are clever enough, you could probably also autogenerate the communication layers on the presentation and business levels.
And if there's something you missed, there's just one program to change (the code generator) and possibly the table definition files. A couple of days' work and you're ready to regenerate the entire data layer. That is a bloody impressive vision, to my mind.
4. Design by Contract. This pervaded my code throughout my time at TurboPower, although it wasn't until I read Bertrand Meyer's book that I started calling it by his term, Design by Contract. Admittedly most of the DBC code I wrote was in the form of preconditions ("hey! you're passing nill for this important object") but it served me well and helped customers understand what they could and couldn't do with my code.
5. Think small, design small, code small. This I had a hard time with in my youth, and certainly whilst I was at TurboPower. I tended to try and think of everything and then design/code the lot in one go. The products that TurboPower wrote and marketed tended to reinforce this way of working: we were trying for extensibility all the time. I finally started to break free of this mindset when I wrote the Deflate compression engine for TurboPower's Abbrevia 3. There it was much easier to code something minimal and check that it worked properly (testing!) before moving on. The reason? Deflate is such a god-awful algorithm; it's just special case after special case; and the engine is hidden behind a stream interface. But the lessons it taught me about agile development have lasted for some three years now: design and write your code in small chunks and test each chunk before going on to the next small chunk. As you move through the project, your test code grows and grows and makes sure that you don't mess things up by making changes elsewhere.
(I'm also glad to report that the Deflate compression/decompression code has only suffered from one bug--I'd forgotten to initialize a buffer to binary zeros--since the release. There have been bugs galore elsewhere in the Abbrevia code, but, apart from that one, the only problem with the engine has been the performance of the decompressor.)
6. Refactoring. There is another benefit to thinking and coding small all the time: you never get so far ahead that you are afraid of refactoring what you have. (I'll freely admit that this practice is fairly new with me: I only started understanding it possibly 18 months ago.) By thinking small, you naturally fall into the habit of writing small classes and small methods. You think in terms of interfaces, of object interactions and roles, of design patterns. You are not afraid of modifying your code to make it simpler, since you have all those tests to back you up. Nowadays, I get really quite nervous when I write a long method, like some teenager worried about getting caught flipping through the mags from the top shelf of the newsagent's: am I going to get caught? The embarrassment of it all.
So, that's my answer to how I code: think small, use test-driven development to write fully tested code, refactor to make the code simpler and more elegant, write code generators wherever possible, use abstraction in the form of interfaces foremost, and class hierarchies otherwise, make sure that I add preconditions at least to my code to avoid problems when others use it. How do you code?