More on this book
Community
Kindle Notes & Highlights
Read between
August 20 - September 18, 2018
we can’t let “best” be the enemy of “better.”
The idea behind refactoring is that we can make software more maintainable without changing behavior if we write tests to make sure that existing behavior doesn’t change and take small steps to verify that all along the process.
When we need to make changes and preserve behavior, it can involve considerable risk. To mitigate risk, we have to ask three questions: 1. What changes do we have to make? 2. How will we know that we’ve done them correctly? 3. How will we know that we haven’t broken anything?
When we change code, we can introduce errors; after all, we’re all human. But when we cover our code with tests before we change it, we’re more likely to catch any mistakes that we make.
Dependency is one of the most critical problems in software development. Much legacy code work involves breaking dependencies so that change can be easier.
When you break dependencies in legacy code, you often have to suspend your sense of aesthetics a bit. Some dependencies break cleanly; others end up looking less than ideal from a design point of view. They are like the incision points in surgery: There might be a scar left in your code after your work, but everything beneath it can get better.
When you have to make a change in a legacy code base, here is an algorithm you can use. 1. Identify change points. 2. Find test points. 3. Break dependencies. 4. Write tests. 5. Make changes and refactor.
A seam is a place where you can alter behavior in your program without editing in that place.
As the amount of code in a project grows, it gradually surpasses understanding. The amount of time it takes to figure out what to change just keeps increasing.
Test-driven development uses a little algorithm that goes like this: 1. Write a failing test case. 2. Get it to compile. 3. Make it pass. 4. Remove duplication. 5. Repeat.
For legacy code, we can extend the TDD algorithm this way: 0. Get the class you want to change under test. 1. Write a failing test case. 2. Get it to compile. 3. Make it pass. (Try not to change existing code as you do this.) 4. Remove duplication. 5. Repeat.
Good design is testable, and design that isn’t testable is bad.
Encapsulation and test coverage aren’t always at odds, but when they are, I bias toward test coverage. Often it can help me get more encapsulation later.
You will find bugs, but usually not the first time that a test is run. You find bugs in later runs when you change behavior that you didn’t expect to.
If we write tests based on our assumption of what the system is supposed to do, we’re back to bug finding again. Bug finding is important, but our goal right now is to get tests in place that help us make changes more deterministically.
Here is a little algorithm for writing characterization tests: 1. Use a piece of code in a test harness. 2. Write an assertion that you know will fail. 3. Let the failure tell you what the behavior is. 4. Change the test so that it expects the behavior that the code produces. 5. Repeat.
many people don’t use them because they are so caught up in trying to understand the code in the most immediate way that they can. After all, spending time trying to understand something looks and feels suspiciously like not working. If we can get through the understanding bit very fast, we can really start to earn our pay. Does that sound silly? It does to me, too, but often people do act that way—and it’s unfortunate because we can do some simple, low-tech things to start work on a more solid footing.
Pragmatic considerations often keep things from getting simple, but there is value in articulating the simple view. At the very least, it helps everyone understand what would’ve been ideal and what things are there as expediencies.
When we simplify and rip away detail to describe a system, we are really abstracting. Often when we force ourselves to communicate a very simple view of a system, we can find new abstractions.
The terrible thing is that the team wasn’t inexperienced. There were some other very good-looking areas of the code base, but there is something mesmerizing about large chunks of procedural code: They seem to beg for more.
At this point in my career, I think I’m a much better programmer than I used to be, even though I know less about the details of each language I work in. Judgment is a key programming skill, and we can get into trouble when we try to act like super-smart programmers.
I have this little mantra that I repeat to myself when I’m working: “Programming is the art of doing one thing at a time.” When I’m pairing, I always ask my partner to challenge me on that, to ask me “What are you doing?” If I answer more than one thing, we pick one. I do the same for my partner.
Superficially, this might look like we’re making things pretty for pretty’s sake, but one pervasive problem in legacy code bases is that there often aren’t any layers of abstraction; the most important code in the system often sits intermingled with low-level API calls.

