Over on the XP mailing list, there's been a long-running thread involving Chris Wheeler and Friedrich Brunzema. (Chris's blog, agilelectric.com, is one of my favorites, by the way.) They're reporting on an experience with continuous design: they tried to do the simplest thing, despite knowing a more sophisticated design was needed, and later found putting the sophisticated design in place to be very difficult due to the overly simplistic assumptions they had made.
From this experience, they've drawn the quite reasonable conclusion that, when you know what the future holds, you should put your infrastructure in place in advance.
Although this is a reasonable conclusion to draw from the experience, I disagree with it. My conclusion is that simple designs--well, all designs, really--should be built for easy change, which in practice means eliminating duplication, maximizing cohesion, and not coding anything you don't need right now. Here are some excerpts from my posts on the subject:
It seems to me that your premise is that some infrastructure is cheaper to build ahead of time. I disagree with that premise, although I can't easily explain why I disagree. Basically, it seems to me that the "infrastructure in advance" argument is based on the idea that some code done with insufficient infrastructure becomes very costly to generalize.
My experience is different: For me, as long I've focused on keeping duplication out, generalization can be added at any time--and in fact, it's cheaper for me to generalize a incorrectly simplistic solution than to fix a misconceived generalization.
In those cases where I have foreknowledge about what's going to be needed, I use that to inform me about duplication to be aware of. For example, if I were concerned about internationalization coming up as a story, I'd be sure to aggressively consolidate formatting code. But I wouldn't add any actual internationalization code.
Your internationalization example is a good one, don't put the feature in, but keep it in the back of your mind, and actually to refactor to put all of your formatting code in one place. If you allow me to be the devil's advocate, I could say that you were speculating ;-). So, where would you draw the line?
And I replied:
I draw the line when my refactorings no longer improve the design I have today. I use my "foreknowledge"--which for me is never 100% correct, even when I think it is--to guide me in making engineering tradeoffs.
Hmm... I'm not sure how to describe this. Let me see if I can. I generally see more opportunities to refactor than I feel I have time to make. After all, I need to deliver software, too. So I pick which refactorings to work on each day based on:
- what I'm working on
- what I see as the biggest risk for the future
- the cost of each refactoring
- my confidence that the refactoring is a good idea
Let's take the internationalization example. Let's say that I'm worried about two aspects of the system's design:
- I'm using basic strings everywhere and in this language basic strings only support ASCII. If I ever want to change the way strings behave, I'll have to change every string throughout the entire system.
- Date and time formatting is handled at the last minute in the presentation layer. Formatting is a simple API call and everywhere we display a date or time we make that API call. But if we want to display the date differently throughout the system, we'll have to change every single one of those API calls and add a culture code.
In this example, the customer hasn't asked for internationalization. I'm concerned about it, so I've asked the customer about it. They said that they only care about english speakers. I've done this before and I'm sure they'll care about other languages once they realize that the Internet gives them a global audience.
In this situation, I plan to eventually refactor away both of the design problems I've identified above. They've crept up on me, though, and now neither one is a quick fix. Using our refactoring slack, we can fix each one over a couple of iterations. So I talk it over with the rest of the team and we decide to fix the primitive obsession problem with strings first. We feel that a request for internationalization will more likely impact this issue than the other. Plus, this one will get worse more quickly if left unattended and it's a tedious but not difficult fix.
We create our own String class and wrap it around the platform's string class. Over the next few iterations, we replace all uses of the platform string class with our own String class.
You asked where I drew the line. In this case, I draw the line here. We've addressed a real flaw in the current design. Before, system-wide changes to string behavior would have been expensive (and only gotten more expensive over time); now, they're cheap. That's not speculation, it's fact, independent of whether the customer ever asks for internationalization support.
I also didn't speculate in that I didn't add Unicode support or any other new behavior to the String class. That's where the line is. Over time, I'm sure some behavior will migrate into the new class--perhaps a replace method, or a split method. Those refactorings will also be driven by observation about duplicate behavior.
Where I did speculate was in my belief that system-wide changes to string behavior would be an issue. Although I would have addressed this design flaw anyway, eventually, my speculation drove me to address it now rather than later. I'm okay with that, because I was going to do some refactoring anyway, and this refactoring seemed to be the best one available. If I was wrong, it's not a big deal--the design really is better and will likely provide a platform for further simplification around string behavior.
One last note. In this example, I assumed I was using a language which doesn't have great built-in String class. In a language like Java, which has a robust String class with Unicode support, regex, etc., I probably wouldn't make my own String class. (But I would keep an eye out for the "primitive obsession" code smell.) In C#, which has Unicode support but isn't as robust as the Java String class, I might or might not make my own String class, depending on the risks I saw.
I should add that I think there's some decisions that really are more expensive to change. But they're things like choice of language rather than regular design decisions.
Many thanks to Chris and Friedrich for bringing up this example and participating so gracefully in a discussion about what they could have done differently.