Can Refactoring Grow Out of Control?

In a recent talk on architecture and agile development, Philippe Kruchten told his listeners that refactoring can grow out of control when people decide not to do up-front design and instead decide to let the "architecture grow with the code", as many agile software developers like to see it.

Let me repeat his (real-life) example:

Agile Development by the Book
On one big project, where Philippe got involved in, the teams were doing agile development by the book: one-week iterations, daily stand-up meetings, iteration planning with user stories, retrospectives, and regular refactoring to change and improve the design of the code. However, despite the fact that the teams were doing just about everything that the agile gods have prescribed, one problem just got bigger and bigger:

The time needed to do refactoring grew with the size of the code.

It appeared that, while the code base grew over time, so did the time needed to refactor the code to accommodate new features. Apparently, the teams were unable to stabilize the architecture. Halfway through the project the iterations had to be lengthened to three weeks just to be able to finish refactorings. And in the end the project simply ground to a complete stop.

What went wrong there?

Should We Not Grow Architecture with the Code?
Philippe Kruchten's interpretation was that it is not enough just to do refactoring and let "the architecture grow with the code". He told us that some work in up-front architectural features is essential. He does not believe you can go on by just adapting all the time. Philippe says that the principle of deferring decisions does not hold true in every case. You also have to anticipate, or else in the end you will spend much of your time reworking everything.

However, my own interpretation is a little different. (This might not be a surprise to my regular readers, as I rarely seem to agree with any of the experts. I can't help it. It's just a gift, I'm sure.) I believe that, most of the time, people simply use the wrong approach to the idea of "deferring decisions". Let me give you an example:

How to Defer Decisions
When I'm building a new application, I will have to decide whether my application is going to be multilingual or not. However, it may be better for me to defer that decision. But if I do, then what shall I do with all those form labels, button texts and abusive error messages that I need to put into my code now? I believe the wrong approach in this case is to simply use static strings all over my code base. Because if I do, I will not seriously have deferred the decision. I will have (unconsciously) favored one outcome of the deferred decision over the other. Because I can be certain that it's going to take a lot of effort to refactor my code later, when the decision finally falls to make the application multilingual. In fact, chances are that it will be too costly to change later, and I will have painted myself into a corner.

To properly defer a decision requires you to (consciously) keep all options open.

What you could do in such a situation is to create a simple factory method that provides strings to the entire application. All button texts, form labels, page titles and silly easter eggs will get their strings from that simple function. Of course, it would still be a single-language static-string solution. The implementation might be one huge ugly motherf*** of a switch statement concealing 10,000 strings! But at least it would be an easy monster to refactor later on in the project.

Philippe explained that…

"Architecture is any binding decision in your code that is hard to change later on."

To that I would like to add…

"The choice not to plan for a deferred decision, is also an architectural decision."

Deferred Decisions vs. Architectural Decisions
I don't know the details of the big project that Philippe was referring to. But I wouldn't be surprised to learn that most of their ever-growing refactorings were a result of not properly applying the principle of deferring decisions. Choosing not to hide the details of string construction from the rest of the code is an architectural decision, that will make it hard for you to switch to multiple languages later on. Choosing not to hide the intricacies of the data layer is also a decision. It will make it almost impossible to add versioning to the data later. And choosing not to hide the implementation of user notifications will mean that a switch from email to SMS or paging is also going to be a lot of work.

The choice not to safeguard your code against the outcome of deferred decisions is an architectural decision in itself. What's interesting is whether you're making those decisions consciously or not!

It Takes a Decision to Defer a Decision
So you see, I agree with Philippe that refactoring in itself is not enough. You have to anticipate and make conscious architectural decisions to let refactoring work for you. But I don't agree with Philippe that "growing the architecture with the code" is somehow not enough. Sure it can be enough.

The problem is not lack of up-front design. It is lack of planning for deferred decisions.

If you understand how to keep your options open, and make the necessary architectural decisions consciously, then I think all should be fine!

Afterword: I just got an email from Philippe Kruchten himself. According to Philippe we're actually "in furious agreement". But I worked on this bloody post for at least two hours, so I'm definately not going to delete it…

Subscribe to this blog with a reader or by email!

Latest, greatest and favoritest posts:
Architects vs. Agilists (1 – 1)
Top 20 Best Agile Development Books, Ever
The Virtue of Junk Code

Related Posts
free book
“How to Change the World”
  • Torbjörn Gyllebringg

    From what I gather you’re in active agreement with some experts. The Poppendiecks phrase this concisly as “defer decisions to the last (responsible) moment” where ironicly enough the emphaisis should really be on that silly word inside the parens.
    As your example highlights, using static strings everywhere is not defering the decision it’s taking a clear bias towards *not* doing localization. Theese sort of issues crop up all over the place and it’s actually a great challange and takes much thought to effectivl defer a decision to the future without at the same time introducing a strict bias towards a single solution.

  • Eric Normand

    I agree.
    Though perhaps only in form. It seems that the agile thing to do would be to write static strings until you notice a pattern (repetition). Then you would factor out an indirection layer like you described above to remove the repetition.
    The indirection is just a way to organize all of the code and to indicate they have a common purpose.
    It doesn’t *have* to be a conscious decision to maybe later internationalize your program—though it could very well. It follows pretty well from the Refactor Mercilessly principle.

  • Jurgen Appelo

    Guys, thanks for your feedback. I think I actually agree. What a weird feeling…
    Eric: as soon as you type a second static string, it qualifies as repetition. Don’t you think? Because every string has the same purpose: communicate something in some language to the user.

  • Rush

    If you choose not to decide, you still have made a choice.

  • Rich McCabe

    I’m not quite certain I follow the logic here. It seems to me, in your example of a potential multilingual feature, is that you didn’t defer anything at all–you chose to architecturally support a multi-lingual feature. Hmm. Perhaps the issue is confused by the semantics of “defer” and “commit”. Allow me to expand on your example. The customer says, “I think I may want this product to be multi-lingual”–that is, a feature, but a low-priority one. So now what? The extreme interpretation of YAGNI is that I do the simplest thing that works for now: no multi-lingual feature committed in this iteration, so use static strings (ignoring for the sake of argument any other reasons we might have for not coding with static strings). Now if your architectural provision for the future multi-lingual feature is a huge imposition on the system, the simple solution is arguably correct–why should we pay for that low priority feature when it is only a future possibility? However, you seem to be saying, I can put in a cheap architectural hook, a factory method, which makes a future mutilingual feature much cheaper, but still defers most of the cost of fully implementing it. Hmmm, again. The question seems to be, how cheap is “cheap”? At what point do we begin to noticeably suffer under the weight and complexity of all these little hooks we’re hanging all over our architecture to defer decisions on future potential features? Do we at least agree that if there is no such feature at all, then we are very far out on a limb in warping our architecture in anticipation of it, regardless? If the customer at least acknowledges the feature, then we can have a discussion about the tradeoff: the cost of “buying an option” now for something that might not happen vs. paying full cost later if it does happen? Beck’s point about YAGNI and hooks is, I think, that you don’t just pay for the hook once–it is an extra twist in the design that hangs around forever, cluttering the place up. I don’t think I’m an ideologue here, and I think I can sympathize with the situation Krutchen describes, but we must be very careful to distinguish between future “must have” and “almost certain to want, surely” features, and how much we are willing to pay to anticipate them.

  • Jurgen Appelo

    Rich, that’s some great feedback. You have some valid points there. But then, if you decide not to pay for the cost of the hooks, then Philippe is right that the refactorings are going to cost you more and more time, as the code base grows.

  • Mike Polen

    Jurgen, if one takes your advice where does one stop? What future possible feature hooks do you not build in? Your advice seems to lead back to BUFD, figure out all possible changes (like we are any good at that) and design hooks for all of them? If a team has increasing refactoring costs it is likely due to an inflexible design. I would guess it is a skill issue. As refactoring time increases I would hope someone would bring it up in a retrospective and ask “Hey, we seem to be spending more time this iteration doing refactoring that doing features. Why is that?” Don’t you think that has a better chance on increasing their performance than designing in hooks that may or may not add value?

  • Jurgen Appelo

    Mike, I think you’re contradicting yourself.
    “if one takes your advice where does one stop? What future possible feature hooks do you not build in”
    You’re right. I wouldn’t know where to stop. They are tiny payments to prevent big payments in refactoring later.
    “If a team has increasing refactoring costs it is likely due to an inflexible design.”
    I agree. But it requires up-front design decisions to keep a design flexible!
    So you say we should refrain from actively making our designs flexible, while at the same time we should keep them flexible?

  • Mike Polen

    I guess the key statement that shows our disagreement is “it requires up-front design decisions to keep a design flexible!” I do not believe that. I used to think that was true, but my experience with iterative architecture has caused me to change my mind. Your design should always be flexible for changes, but only for changes that are needed now, not some perceived need in the future that might never be. The how is using design principles that are too numerous and context sensitive to fit into a comment. Given that we disagree on this, where do you think I contradicted myself? I have no problem disagreeing with you, but if I disagree with myself then I become worried 🙂

  • Jurgen Appelo

    “Your design should always be flexible for changes, but only for changes that are needed now, not some perceived need in the future that might never be.”
    Mike, this is the crucial statement of yours that I either don’t understand or don’t believe.
    When my working hours are “flexible” it means that they accommodate anticipated variation: in the future. Not just today’s variation.
    Anything that is “flexible” is, by definition, designed to withstand future changes (up to a pre-defined degree).
    My today’s agenda happens to allow the sudden change of my friend asking me to go see a movie. It fits in my schedule *now*. But that doesn’t mean that my agenda is designed to be flexible. It just turns out that way. (I have to say no lots of times, I’m afraid.)
    So, there’s our disagreement. I think what you’re saying sounds illogical. I think there’s no such thing as flexibility only for the current moment. But as I said, the other option is that I just don’t understand it. 🙂

  • Mike Polen

    No design is flexible to all changes, but if you understand your technology and your domain you can make your design flexible in ways that it needs to be flexible. I believe you can do this without extra hooks. Design is something you should always be doing and with the tests to back you up and design principles in your hand, you can incrementally build your software without refactoring ever dominating your iteration. I believe in the YAGNI ( which I think is something you do not believe in. It is good that we disagree: “When two men in business always agree, one of them is unnecessary.” -William Wrigley Jr.

  • Jurgen Appelo

    “you can make your design flexible in ways that it needs to be flexible”
    This sounds like you agree that you actually have to do something to make it flexible.
    I definately believe in YAGNI. I just think that even deferring decisions requires a tiny bit of planning.

  • Brian Goodhart

    Using the example at hand, we have a conversation with the Product Owner: “You want us to build this without a multilingual capability. Here are your choices:
    1. Build a single-language application today. In terms of future multilingual capability, apply the YAGNI principle.
    2. Build a single-language application today, but spend a little more of your (Mr. Product Owner) money to ensure that a multi-language capability can be added later.
    3. Build today with multi-language capability.
    So we have these conversations with the Product Owner, as we always do in the constant process of fleshing out features, and the Product Owner makes an informed decision, understanding the tradeoffs, among options 1, 2, and 3.
    Perhaps the “capability to expand” implicit in option #2 is a separate feature that receives its own position on the prioritized feature list.

  • Rich McCabe

    I like Brian’s point. A financial trader would certainly grasp that proposal–she would call it “buying an option”. This works when the lower-priority features predict the future evolution of the architecture. But was that what was going on with Krutchen’s project? Were they deliberately ignoring the implication of features scheduled for later iterations? I assume it was–else what would have been their basis for anticipating a different design than the one they started with?
    Many people have suspected some slide-of-hand in YAGNI–the claim was that the “simplest thing that works (now)” is also the “the most flexible design that doesn’t overcommit to anticipated features.” Many have questioned how this can be, and suspect YAGNI proponents of subtly biasing their design choices toward anticipated changes. I admit to being one of those–thinking that YAGNIists (forgive me) are deliberately overstating their case to counteract the historical tendency to overbuild the design. But Beck and others, especially Jeffries, have consistently challenged all comers to show them a counterexample. A fair position, and we probably have reached the point where even more large scale examples than the one Jurgen provides above are necessary to make any further progress on this issue. Apparently, Krutchen thinks he has one. Ambler has claimed as much, also, noting that he has seen some teams waste time “discovering” that they needed to use a commercial database in their application. (However, I might rule that one as a situation where they ignored the implication of “must-have” features; whereas here I think we are debating how to address “might-need” features).
    Perhaps, in every real situation there is more than one possible “simplest thing that works”? And this gives us enough wiggle room to avoid “death by refactoring”?

How to Change the World - free Workout - free