Since half a year, I'm reading a lot about "modern" software engineering
approaches, like agile development practices, XP and test-driven
development. Read on for a short story how I work today.
This is how I
write my code currently, and I'm pretty productive that way, but your
mileage may vary.
Usually, I'm working in short sprints nowadays, I try
to keep those sprints around 2 hours. Before I start a sprint, I think
about what I want to have at the end, usually some specific
functionality. It's just a rough idea what should work, no concept
paper, though I tend to write up a short note before implementing a
larger system, covering the main classes and their responsibilities.
the beginning, I just write a skeleton class header and define the types
I think I'm gonna need, and the helper objects definitions, just enough
that it compiles. There is no code that does something, so this is
usually really quick (like 5 minutes or so). Note that I don't implement
the functions so calling them will result in a link error.
Then, I start
to write unit tests for helper classes, one for each function, of course
only if there are helper classes that I'm aware of beforehand (often,
the helper classes appear during a later refactoring). This takes me
around 10 minutes or so per function, including the unit test. If I end
up using some other function, I'm getting a linker error so I can see if
I called the function properly or whether I thought the syntax is
different (this happens most of the time - you have a function called
doX in your interface but in your test case you think it's called
getQ, this is your chance to fix this, most of the time you were wrong
when defining the interface), and I can go ahead and implement the
missing ones right away. I check in the code at each step, meaning once
after the interface, then after each test, then after implementing the
function, and then after some bug fixing or test expanding.
to keep the tests simple, just a few lines, for most helper classes,
they are 2-3 lines long at most. I try to test border cases, too, it's
quite natural to have one test like "PopStack" and write a
"PopEmptyStackThrows" just along with it to see what happens. Here, a
crash costs you maybe a minute to fix it, later, you could end up
spending an hour tracking it down. And for some reason or another, it is
fun to test such border cases :)
After the functionality is pretty much
done, I do a refactoring pass. During the implementation, I often end up
with duplicate code, and in this step, I try to clean it up as much as
possible. With the unit tests in place, I can be pretty sure that I
don't break stuff in this phase. I add a few comments now for stuff that
is not obvious. Basically, the goal is to have solid and polished code
that is suitable for further development, but it's not top-notch as it
has been just used by the unit tests mainly and not by "real" code.
soon as the helper classes are in place, I start with the main class. I
try to focus on a single function, just as with the helper classes, and
get this working. I try to have at least two tests for each function,
one where it works, and one which shows what happens in case of an
error. The latter one is actually the more important one, as it is the
one which makes the code robust, as you tend to forget what will be
treated as an error usually. It's actually just the same as with the
helper classes. I tend to go bottom up by using only functionality that
I have tested already. This means usually to start with the simplest
functions first and work towards the complex ones. If I need a new
helper function, I test it first before using it somewhere else, so I
have less trouble if a test fails.
The last step - as soon as the whole
class works - is yet another refactoring step to consolidate
functionality and extract utility functions. In this step, I usually
polish the docs, document invariants, and add further checks for error
cases (with tests). After this step, I consider the code production
ready and move on - the code in this stage has each function documented,
with pre/post conditions, unit tests for each function, has been
refactored once or twice, and is of pretty high quality.
In case I
discover a bug later (by other code that depends on this class or
functionality), I always write a test case for the affected class first
(and check it in) before fixing it. I also look through all similar code
to see if some other part is affected by the same bug (this is usually a
sign that some refactoring is needed).