Refactoring Inhibitors

source: Jeff Langr & Tim Ottinger
font: Daniel Black

Refactoring is perhaps the most significant part of sustaining a system in an agile environment. Entropy in a system is reality: Code degrades unless you make strong efforts to stave off the degradation. You need good controls (tests) that provide rapid feedback in order to effectively keep a system clean. If the system attains a state where its design is poor, maintenance costs can become an order of magnitude larger. This can happen more rapidly than you think.

Agile can exacerbate poor practice. You don't do a lot of up-front design, and you constantly add new features that were not pre-conceived in an original comprehensive design. Repeatedly forcing features into a design will result in a disaster unless you have a means of righting the design over time. That's what refactoring is about.

With the need to avoid degradation, it's important to recognize anything that might prevent a team from refactoring as much as they must. This card provides some inhibitors to refactoring that you should watch for.

  • Insufficient tests. Experienced developers know when they do the wrong thing, something that degrades the quality of the code. Yet much of the time, they don't follow up and fix the problems they just created. Why not? Too much fear over shipping a defect. "It ain't broke, don't fix it." They don't want to do the right thing, because that would require mucking with code that's already working--code that's not even "their" code.

    The right thing is to retain a high-quality design through continual incremental refactoring, which requires the confidence to change the code. That confidence derives from very high levels of unit test coverage, which you can obtain through TDD. You won't get the confidence from test-after development (TAD), which at best nets around 70% (and often the complex areas are in that 30% of uncovered code). TDD enables confident refactoring.
  • Long-lived branches. The right thing is to ensure the system has the best possible design through continual refactoring. But developers working on a branch want to avoid "merge hell," and will plead for minimal refactoring as long as the branch exists. Branches should be short-lived.
  • Implementation-specific tests. "Changing the design of existing code" should not create the need for a lot of test rework, particularly if you are changing details not publicized through the class interface. The need to mock generally exposes information to a client (the test) that could otherwise remain private. The use of mocks should be isolated and abstracted. Make sure you're refactoring your tests! Minimize test-to-target encapsulation violations created by mocks.
  • Crushing technical debt. If you've not refactored enough, you'll soon be faced with a daunting challenge--code rampantly duplicated throughout the system, long (or short!) inscrutable methods, and so on. Once a problem gets bad enough, we tend to look at it as a lost cause and throw our hands up into the air, not knowing where to even begin. Don't let technical debt build up--refactor incrementally, with every passing test!
  • No know-how. Understanding how to properly transform code is one educational hurdle. Knowing if it's a good move or not requires continual learning about design. Developers without significant background in design will be reluctant to refactor much, as they're not sure what to do. Learn as much as you can about design, but start by mastering the concept of Simple Design.
  • Premature performance infatuation. The goal of refactoring is better design, which to most means more cohesive and decoupled. That means a good number of small classes and small methods. A simple refactoring, like extracting a method solely to improve cohesion and thus understanding of code, can frighten some programmers. "You're degrading performance with an unnecessary method call." Such concerns are almost always unfounded, due to things like HotSpot and compile-time optimization. A true performance expert, working on a system with some of the highest transactions in the world, backed me up on this one. Make it run, make it right, make it fast. -Kent Beck (and make it fast only if you measure first and after)
  • Management metric mandates. Management governance (wow, do I hate that word) by metrics can have nasty, insidious effects. Examples:

    1. "You must increase coverage by x percent each iteration." Actual result: Developers tackled each story with a hacked-together written integration test, not a unit test, that blew through as much code as possible. Developers then hastily created new tests by copy-paste-vary. No time left to refactor--they just need to hit their coverage numbers! Later, changes to the system would break many tests at once. Since the tests were barely comprehensible, developers began turning them off.
    2. "We need to reduce defect density." Defect density = defects / KLOC. Well, anything based on lines of code is useful only as far as you can throw it, and you can't throw code (the bits fall everywhere). You can improve defect density by reducing defects. Or, you can increase the amount of code. Most programmers aren't as evil to deliberately create more code than necessary. But if you say to your pair, "hey, we should factor away the duplication between these two methods that are 500 lines each," there will be either a conscious or subconscious decision to resist, since it worsens the metric.
    Programmers will do whatever it takes to meet bogus mandates on metric goals. Use metrics to help uncover problem areas, not dictate absolute goals.
From a technical perspective, few things will kill an agile effort more certainly than insufficient refactoring.