Þ   briarpig  » tech  » refactoring


Code refactoring is an often discussed topic in software development these days. The word refactoring is almost a buzzword as a result of hype from agile programming. Perhaps I'm using the word in a different sense. In any case, this page describes how I refactor code.

28jul07 intro

     I first drafted this material last April 2007 when folks at work asked how I managed several evolutionary changes I coded over recent months. The project director wondered if I could teach others to do whatever it was I did with such good results.

     Specifically, could I call a meeting? Could I give one or more talks on how I refactor code? I said I wasn't actually following a system or plan. My approach was intuitive: I wouldn't know what to say unless I first worked out what I was doing. So I sat down and drafted what you see below while thinking about how I reason while refactoring.

     I posted this on a wiki page at work, but no one has read it as far as I know. It seemed a shame for it go to waste — I thought it had some value. So I decided to repost it here since it contains no information of a sensitive nature. I cut original leading and trailing sections with context and examples in terms of code at work. What remains is generic code surgery technique.

     Note I'm not rewriting this to jazz it up. Maybe it could use polish; but what do you want for a couple hours of writing? Of course all the html and css markup is new.

april07 refactoring

what

     What is refactoring, exactly? And what needs refactoring? What problem is solved by it?

     Refactored code is just re-arranged code that does the same thing, but with a different organization, with new fracture lines you add for enough flexibility to replace something that needs an alternative (for one or several reasons).

     You refactor code when you re-arrange the order in which things happen (and re-arrange which objects handle requirements) so you can later replace one solution with another. The goal is usually to isolate some irritant in one place, so you can try another way to handle the irritant. Ugliness does not warrant refactoring — cosmetic issues have no priority.

     A current code irritation sufficient to get priority is usually some combination of these, when you're stuck:

  • too slow or too big (uses too many cycles or too many bytes) and it must come in as less
  • too buggy or too squirrelly, or can't be audited to assure quality and stability
  • can't tell whether one component interferes with another component
  • the proper strategy to use in a complex situation is unclear
  • one component confuses understanding another when both are mixed together
  • the cause of undesired behavior cannot be attributed to a specific bad component

entanglement

     Refactoring solves the problem of entanglement: when objects or processes are interleaved in a manner that prevents you from dealing with one without the direct or indirect interference of the other. You can re-arrange code to break unnecessary dependencies, to permit dealing with one thing without spending time on something that should be unrelated.

why

     Why (and when) would you want to refactor code?

     Almost never in practice, because there's usually something more important to do. You only want to refactor a bit of code when that code contains your top priority task or problem, and you can't make progress (that you know will work) or answer a question without refactoring. Some improvements are very hard to assess without comparing alternatives. So sometimes you're best tactic is to try more than one and make an empirical comparison. Refactoring is sometimes the answer when the question was, "Should I change the code from old X to new Y, or to new Z?" and you don't know the answer, but it's important that you end up using the better of the two alternatives Y and Z.

     Some folks feel, when using an object oriented language, that situations should never arise where objects are interlinked in a byzantine dependency network. Shouldn't information hiding be enough to prevent one object from knowing too much about another? In practice, the use of information hiding and abstraction too aggressively will limit how far you can see and how well you can cleverly optimize information that crosses object boundaries. Visibilty is both a performance enhancer and a code organization clarifier. But you can get tied down accidentally. Refactoring typically adds some hiding and removes some visibility in order to make a replaceable component truly pluggable when previously it was hard linked.

     When more than one of the things below is true, it can be a sign refactoring might help:

  • it's hard to see where one feature stops and another begins, from method to method and object to object
  • it's unclear which lines of code are for which effect, or whether there is organic overlap in features
  • nothing can be changed in one place without compile and link ripple effects for long distances
  • the actual code organization shows no sign of the organization present in English descriptions
  • a change that sounds easy when said in words is actually quite hard when looking at the code
  • policies about whether to do things are intermingled with the mechanisms for how to do things
  • pointers to physical implementation details in one subsytem are visible far away in another subsystem
  • callers overspecify what they want done by spelling out exactly how the callee can do it in micro detail
  • implementations can't redo memory managment because code uses outside know too many internals

how

How do you go about refactoring code?

     Now there's the rub. There must be some trick to it, or otherwise it would be easier and it would happen more often. But the problem is refactoring can be confusing, if only because the original version of some code contributes a lot of confusion itself. Even so, no matter how confusing it gets, you can still follow some rules of thumb to help avoid totally wasting your time. With some care, you can reduce the odds your refactoring will bomb and be thrown away. The most important rules of thumb are the first two:


1. keep the system in a working state at all times

     As soon as you make a change that requires any debugging, the odds you'll throw away your work under time pressure go through the roof. Try to make changes you know have exactly the same behavior at runtime, and don't change too much at one time. Assume you'll be interrupted at any time, and that you'll need to go with whatever you have checked in last. You'll never come back to anything that's only half done. If you do, the world will have moved on, anyway.


2. make small incremental changes you can clearly check still work

     During initial stages of refactoring, you want identical results at runtime as much as possible. As a result, you should aim to change as little code as possible, no matter how tempting it is to make cosmetic changes. All you want to do is move the code around so the new arrangement lines things up so a new aisle appears allowing you to move more chairs around later. At first all you want is breathing room, by making it possible to do something else, but without actually doing something else.

     Really what you're doing is removing inconsistencies with a future approach, without altering a past approach beyond mere re-organization. Let's say the old code insists on the existence of an appendix and kidneys. But it doesn't need them entangled — they are entangled only accidentally. So you can disentangle them without breaking the old world, while preparing for a future world that wants them completely separate.

3. focus on what you wish was true

     Try to define a clear view of what the code should be doing, but isn't. A good description probably resembles the way developers speak in plain language among themselves. A recipe for success starts with you knowing what the refactored code should do. Imagine an object in isolation doing the right thing. Write down some sentences that look like: There exists an object Foo which handles issues Bar and Whatever. These are probably the sentences you use anyway when explaining the system to other folks. It's just the current system doesn't actually work like that, exactly.

     With your future view of a better organization in mind, make subtle changes in the code that fit the new order you imagine, where this doesn't interfere with the old system.


4. abstract the code service you imagine being variable and replaceable

     Suppose there's some feature in the old system you want to keep, but kept only with the option of letting you do it differently with a new implementation if you wish. Write an abstract class API (more than one if it's absolutely necessary, when one alone won't work) that summarizes what the feature does, and how other objects in the system request parts of this service. Your abstraction has to cover both the old way and some new way you imagine might replace the old way. You can nudge the old code in a new direction if necessary, so the actual old system is still covered by the new abstract class.

     Abstraction is harder than it sounds, and is hard to describe as a recipe to follow. However, it's usually necessary to see how an old api gets used, by reviewing every place in the system any symbol in the old api appears. Every variable, method call, and object instantiation must be replaceable with any new front you plan to put on the old api.


5. partition knowledge about details — known inside the api but unknown outside

     This is a narrow application of general object oriented desire for data hiding. Rather than a tidy and perfect total hiding of all information, merely aim to hide some specific thing in particular that must be under the control of your new api.

     In order to make some detail X replaceable by subclasses of your api, make knowledge of this detail known only inside the subclass, but unknown outside the api. For example, if you want to hide whether files are memory mapped or not, write a File api that knows about this internally, but doesn't allow anyone outside to know if files are memory mapped.


6. push complex high level operations down into the new abstract api

     In some places where old interface is used, complex high level operations might occur that look like obvious problem areas when replaced by a new api, especially when those high level uses make assumptions about low level internals, or when efficiency would be impacted by loss of information across a new abstract interface that hides too much.

     Instead of tuning the api to make the high level operation expressible using the api, just push the high level operation itself down into the new abstract api. As long as you can organize inputs and outputs to a high level operation in a rational way, feel free to lump complex requirements for such things with everything else in the abtract api. (This might look slightly ugly, but pretty appearances are not an objective in this exercise.)


7. subclass the abstract api to write an implementation of the old system

     This effort is a translation of the old code to the new interface, so exactly the same code does everything, but now presented with the new interface that will later let you replace it with an alternative. When you're done with this step, you'll have two versions of the old sytem — one that's still called the old way, and a new one under the new api that's not yet actually used in the system. It's all the same old code, just folded differently.


8. replace uses of the old api with the new api's first subclass wrapping the old code

     This step is one that has high risk of wrecking all your plans, sending you back to the drawing board (if you're lucky) or killing your new api plans outright. You really want this change to work the first time, if possible. This is one of the reasons you should have changed as little as possible in the old code while putting it under the new api. The only thing that should be different is how the old code is accessed.

     If this step breaks down, it means you either translated the old code to the new api incorrectly, or your reasoning about the new api covering all the old behavior was wrong. You don't have time to be wrong. Failure to pass tests is a generalized sign you're screwing up, or don't know what you're doing, and is very likely to get the plug pulled on refactoring (especially if there's any competing alternative in the wings). So do the simplest thing that recovers old code in the new api.


9. use a config setting to choose either the old code or a new replacement

     Now that the old code is presented by the abstract interface, it can be replaced. But make it the default unless someone (that's you of course) requests a new version instead.


10. experiment with a new implementation that replaces the default in your config

     You can even run with both the old and new versions of the code, but on different machines. Any new replacement you write fits directly into the spot where the old code is called. Checkin snapshots of your new subclass when results look stable to you, but leave the old code as the default used by everyone else. When you write an alternative with apparently better results than old code, schedule tests with the new replacement as the default.


11. test new replacements with unit tests, if possible, preferrably in isolation

     Often one desires to replace part of a system in an attempt to get more control over something that's historically hard to analyze and debug. If you write a replacement with a smaller number of dependencies than an original, you might be able to test the new version in isolation. (Or you might be able to test a simplified version in isolation.)

     If possible, write the new abstract api to depend on the smallest number of things since all those things will need to be present when you write a test. If you err on the side of depending on too much, you won't be able to run your replacement in any other context than a full running system.


13. switch to the new version when it clearly outperforms the old code

     Make the current best known subclass the default used by folks who don't specify a specific version in the config file. But be able to switch back again with trivial effort without rebuilding the system, by changing a config file parameter. The next observed system problem will be blamed on your change because it happened last. Let folks easily run the old version of the code instead, so they can see if your change has any effect on observed problems.


14. add statistics that reveal what's new about the replacement implementation

     Other folks might have trouble seeing the same signs you do. Even you might have trouble telling the difference between old and new implementations without a steady trickle of data summarizing what's good or bad about current subsystem results.

     Try to make the evaluation of your changes evidence-oriented by making evidence available.


15. preserve the old version of the code for comparison in the future.

     Having more than one version of code for an abstract interface allows you to swap them in and out when diagnosing strange behavior in a system.