The (current) canon of dogmatic Agile (with a capital ‘A’) and test-driven development demands that we RefactorMercilessly at every turn, but that seems somewhat simplistic to me. When I refactor, I do indeed strive to refactor mercilessly; but sometimes I don’t feel the need to refactor at all…
Given a suite of acceptance tests for a software system, there is clearly a (countably) infinite set of possible program sources that will pass those tests. (If you don’t see that, consider that every variable in the program has an infinite set of possible names; the same is also true of function parameter ordering, curly brace positioning, sorting algorithm, method breakdown, class naming, whitespace…) And as the system grows during development and maintenance, we navigate this infinite space of programs. Every time we write, change or remove a line of code, our system moves to a new point in that space; every time we add or change an acceptance test, we define a new (countably infinite) space of possible programs, and our system instantly sits at one point in that space.
And so every time we add a new feature, or fix a defect, we have an infinite number of possible points to which we could move our system within the program space and still pass all the acceptance tests. Furthermore, these “neighbouring” points are not all equivalent: the points in the program space have attributes, or qualities. Each program possesses different amounts of readability, maintainability, resistance to certain directions of change, coupling, cohesion, and so on. Which means that each time we write a line of code we make choices about the qualities of the resulting system. And all too often we forget to make those choices consciously.
Now those qualities I mentioned all have one thing in common: they directly influence both the speed and cost of change of the system. That is, each point in the program space, given a fixed set of acceptance tests, will support some kinds of change cheaply and other kinds more expensively. So as we develop and maintain we should consciously select programs which align with our system’s intended lifecycle; in particular, a write-once-and-forget-about-it system will need different cost-of-change qualities than a system that is intended to survive and be supported for more than a few months.
Refactoring is the means by which we move around the program space. As soon as we have a passing test, we can cast around for a nearby program whose qualities are a good-to-great match for our business needs. How much time we spend doing this is an investment trade-off against those future needs, and should be judged accordingly. Sometimes any old thing is fine; other times it is worth chasing down every last ounce of duplication.
The point of all this is that every day the programmers make choices which affect the system’s qualities. We should do so consciously, and according to the needs of our business and of our users.