There’s a point that I made in the book but which I have had to point out to people a few times since then, and so I wanted to emphasize it a bit more.
When you clean up code, you are always doing it in the service of the product. Refactoring is essentially an organizational process (not the definition of “organizational” meaning “having to do with a business” but the definition meaning “having to do with putting things in order”). That is, you’re putting in order so that you can do something.
When you start refactoring for the sake of refactoring alone, refactoring gets a bad name. People start to think that you’re wasting your time, you lose your credibility, and your manager or peers will stop you from continuing your work.
When I say “refactoring for the sake of refactoring alone,” what I mean is looking at a piece of code that has nothing to do with what you’re actually working on, saying, “I don’t like the way that this is designed,” and moving parts of the design around without affecting the functionality of the system. This is like watering the lawn when your house is on fire. If your codebase is like most of the codebases I’ve seen, “your house is on fire” is probably even an appropriate analogy. Even so, if things aren’t that bad, the point is that you’re focusing on something that doesn’t need to be focused on. You might feel like you’re doing a great job of reorganizing the code, and probably you are, but the point of watering your lawn is to have a nice lawn in front of your house. If your refactoring has nothing to do with the current product or feature goals of your system, you’re not actually accomplishing anything other than re-ordering something that nobody is using, involved with, or cares about.
So what is it that you want to do? Well, usually, what you want to do is pick a feature that you want to get implemented, and figure out what you could refactor that would make it easier to implement that. Or you find an area of the code that is frequently being worked on and get some reorganization done in that area. This will make people appreciate your work. It’s not just about that—it’s really about the fact that they will appreciate it because you are doing something effective. But getting appreciation for the work that you’ve done—or at least some form of polite acknowledgment—can help encourage you to continue, can show you that other people are starting to care about your work, and hopefully help spread good development practices across your company.
Is there ever a time when you would tackle a refactoring project that doesn’t have something directly to do with the work that you have to do? Well, sometimes you would refactor something that has to do indirectly with the goal that you have. Sometimes when you start looking at a particularly complex problem, it’s like trying to pick up rocks on the beach to get down to the sand at the bottom. You try to move a rock, and figure out that first, you have to move some other rock. Then you discover that that rock is up against a large boulder, and there are rocks all around that boulder that prevent it from being moved, and so forth.
So within reason, you have to handle the issues that are blocking you from doing refactoring. If these problems get large enough, you will need a dedicated engineer whose job it is to resolve these problems—in particular the problems that block refactoring itself. (For example, maybe the dependencies of your code or its build system are so complex that nobody can move any code anywhere, and if that’s a big enough problem, it could be months of work for one person.) Of course, ideally you’d never get into a situation where your problems are so big that they can’t be moved by an individual doing their normal job. The way that you accomplish that is by following the principles of incremental development and design and always making the system look like it was designed to do the job that it’s doing now.
But assuming that you are like most of the software projects in the world who didn’t do that, you’re now in some sort of bad situation and need to be dug out of the pile of rocks that your system has buried itself under. I wouldn’t feel bad about this, mostly because feeling bad about it doesn’t really accomplish anything. Instead of feeling bad about it or feeling confused about it, what you need to do is to have some sort of system that will let you attack the problem incrementally and get to a better state from where you are. This is a lot more complex than keeping the system well-designed as you go, but it can be done.
The key principle to cleaning up a complex codebase is to always refactor in the service of a feature.
See, the problem is that you have this mountain of “rocks.” You have something like a house on fire, except that the house is the size of several mountains and it’s all on fire all the time. You need to figure out which part of the “mountain” or “house” that you actually need right now, and get that into good shape so that it can be “used,” on a series of small steps. This isn’t a perfect analogy, since a fire is temporary, dangerous, and life-threatening. It will also destroy things faster than you can clean them up. But sometimes a codebase is actually in that state–it’s getting worse faster than it’s getting better. That’s another principle:
Your first goal is to get the system into a place where it’s getting better over time, instead of getting worse.
These are practically the same principle, even though they sound completely different. How can that be? Because the way that you get the codebase to get better over time instead of getting worse is that you get people to refactor the code that they are about to add features to right before they add features to it.
You look at a piece of code. Let’s say that it’s a piece of code that generates a list of employee names at your company. You have to add a new feature to sort the list by the date they were hired. You’re reading the code, and you can’t figure out what the variable names mean. So the first thing you’d do, before adding the new feature, is to make a separate, self-contained change that improves the variable names. After you do that, you still can’t understand the code, because it’s all in one function that contains 1000 lines of code. So you split it up into several functions. Maybe now it’s good enough, and you feel like it would be pretty simple to add the new sorting feature. Maybe you want to change those functions into well-designed objects before you continue, though, if you’re in an object-oriented language. It’s all sort of up to you—the basic point is that you should be making things better and they should be getting better faster than they’re getting worse. It’s a judgment point as to how far you go. You have to balance the fact that you do need to make forward progress on your feature goals, and that you can’t just refactor your code forever.
In general, I set some boundary around my code, like “I’m not going to refactoring anything outside of my project to get this feature done,” or “I’m not going to wait for a change to the programming language itself before I can release this feature.” But within my boundary, I try to do a good job. And I try to set the boundary as wide as possible without getting into a situation where I won’t be able to actually develop my feature. Usually that’s a time boundary as well as a “scope of codebase” (like, how far outside of my codebase) boundary—the time part is often the most important, like “I’m not going to do a three-month project to develop a two-day feature.” But even with that I balance things on the side of spending time on the refactoring, especially when I first start doing this in a codebase and it’s a new thing and the whole thing is very messy.
And that brings us to another point—even though you might think that it’s going to take more time to refactor and then develop your feature, in my experience it usually takes less time or the same amount of time overall. “Overall” here includes all the time that you would spend debugging, rolling back releases, sending out bug fixes, writing tests for complex systems, etc. It might seem faster to write a feature in your complex system without refactoring, and sometimes it is, but most of the time you’ll spend less time overall if you do a good job of putting the system in order first before you start adding new feature. This isn’t just theoretical—I’ve demonstrated it to be the case many times. I’ve actually had my team finish projects faster than teams who were working on newer codebases with better tools when we did this. (That is, the other team should have been able to out-develop us, but we refactored continuously in the service of the product, and always got our releases out faster and were actually ahead in terms of features, with roughly the same number of developers on both projects working on very similar features.)
There’s another point that I use to decide when I’m “done” with refactoring a particular piece of code, which is that I think that other people will be able to clearly see the pattern I’ve designed and will maintain the code in that pattern from then on. Sometimes I have to write a little piece of documentation that describes the intended design of the system, so that people will follow it, but in general my theory (and this one really is just a theory—I don’t have enough evidence for it yet) is that if I design a piece of code well enough, it shouldn’t need a piece of documentation describing how it’s supposed to be designed. It should probably be visible just from reading the code how it’s designed, and it should be so obvious how you’d add a new feature within that design that nobody would ever do it otherwise. Obviously, perfectly achieving that goal would be impossible, but that’s a general truth in software design:
There is no perfect design, there is only a better design.
So that’s another way that you know that you’re “bikeshedding” or over-engineering or spending too much time on figuring out how to refactor something—that you’re trying to make it “perfect.” It’s not going to be “perfect,” because there is no “perfect.” There’s “does a good job for the purpose that it has.” That is, you can’t even really judge whether or not a design is good without understanding the purpose the code is being designed for. One design would be good for one purpose, another design would be good for another purpose. Yes, there are generic libraries, but even that is a purpose. And the best generic libraries are designed by actual experimentation with real codebases where you can verify that they serve specific purposes very well. When you’re refactoring, the idea is to change the design from one that doesn’t currently suit the purpose well to a design that fits the current purpose that piece of code has. That’s not all there is to know about refactoring, but it’s a pretty good basic principle to start with.
So, in brief, refactoring is an organizational process that you go through in order to make production possible. If you aren’t going toward production when you refactor, you’re going to run into lots of different kinds of trouble. I can’t even tell you all of the things that are going to go wrong, but they’re going to happen. On the other hand, if you just try to produce a system and you never reorganize it, you’re going to get yourself into such a mess that production becomes difficult or impossible. So both of these things have to be done—you must produce a product, and you must organize the system in such a way that the product can be produced quickly, reliably, simply, and well. If you leave out organization, you won’t get the product that you want, and if you leave out production, then there’s literally no reason to even be doing the refactoring in the first place.
Yes, it’s nice to water the lawn, but let’s put out some fires, first.