Okay, you got the time and management approval to refactor some code. What do you do with it? Refactoring legacy code can be like unraveling a knotted rope and it can be hard to figure out where to start. Here are seven strategies for determining what to refactor.
1. Remove dead code
Dead code is code that was either commented out or is unreachable. I used to hesitate to delete dead code from projects until I saw what a distraction some of my dead code was to another developer reading what I wrote. That changed my mind. Removing dead code will not affect execution, but will make programs easier to read through. Now I unabashedly remove dead code from projects whenever I see it.
2. Add seams to add tests
One of the most valuable things to do with legacy code is to add tests to support further reworking of the code. But often, legacy code is so intertwined it’s difficult to isolate what needs to be tested. Michael Feathers shares a series of techniques for adding seams to make legacy code more testable in his book Working Effectively with Legacy Code. These techniques make software more independent and simpler to test.
3. Make methods more cohesive
Perhaps the two most important and useful refactorings are Extract Method and Extract Class. Developers tend to make their methods do too much. Other methods and sometimes entire classes can be lurking in long methods. Break up long methods by extracting new methods from little bits of functionality that you can name. Uncle Bob Martin says that ideally methods should be no longer than four lines of code. While that may sound a bit extreme it’s a good policy to break out code into smaller methods if you can write a method name that describes what you’re doing.
4. Make classes more cohesive
Another typical problem with legacy code is that classes try to do too much. This makes them difficult to name. Large classes become coupling points for multiple issues, making them more tightly coupled than they need to be. Hiding classes within classes give classes too many responsibilities and makes them hard to change later. Breaking out multiple classes makes them easier to work with and improves the understandability of the design.
5. Centralize decisions
As classes and method become more cohesive it’s possible for business rules to become spread out across a system, making it difficult to read and modify. Try to centralize the rules for any given process. Extract business rules into factories if at all possible. When decisions are centralized it removes redundancies in code, making it more understandable and easier to maintain.
6. Introduce polymorphism
We want to introduce polymorphism when we have a varying behavior that we want to hide, for example, the way we sort a document or compress a file. If I have more than one way of doing a task and I don’t want my callers to be concerned with which variation they’re using I may want to introduce polymorphism so that I can allow new variations to be added later that existing clients can use.
7. Encapsulate construction
An important part of making polymorphism work is based on clients using derived types through a base type. Clients call sort() without knowing which type of sort they’re using. Because we want to hide from clients the type of sort they’re using the client can’t instantiate the object. So we give the object the responsibility of instantiating itself by giving it a static method that invokes new on itself, or by delegating that responsibility to a factory.
Refactoring code is a necessary part of development. It drops the cost of three things: adding unit tests, accommodating new features, and further refactoring. They say hindsight is 20/20 and refactoring code can take advantage of that fact to clean up our design and make code more maintainable.
Previous Post: « Seven Strategies for When to Refactor
Next Post: Seven Strategies for Measuring Value in Software »