Last week, I wrote about methods with consistent return values, and how they’ll make your code simpler. But how does that happen? How can the right refactoring make your code easier to work with? What makes good abstraction good, and bad abstraction bad? I mean, you can’t just blindly rearrange your code and end up with quality software.
If you can learn why these techniques work, you’ll be better able to understand where your code needs help. And you’ll be able to use the right tools where they matter most.
What’s the hardest problem in software development?
“There are only two hard problems in Computer Science: cache invalidation and naming things.”
– Phil Karlton
Sure, that quote is everywhere. But there’s actually a harder problem in real-world software development: managing complexity. Complexity in software development has a few different definitions. At its core, though, the complexity of a program is the amount of stuff you have to keep in the front of your mind while you’re working on it.
As you write more software, you’ll be able to keep more stuff in your head at once. But even the best devs have limits. If your project is too complex, you’ll overload what your mind can handle, and you’ll start forgetting about different areas of your program. And the worst bugs show up when you make a change in one part of your project without realizing how the change will affect a totally different part of the same project.
Simplifying through assumptions
If you had photographic memory, and could remember how all the different parts of your program fit together, you’d have a lot fewer bugs, right? Memory can be hard to build. But you can get a lot of the same benefits from being able to focus on one thing at a time.
Many best practices in software development are about reducing the number of things you have to keep in your mind at once. A few examples:
-
Consistency in your return values means you only have to think about how to deal with one kind of data, instead of different kinds of data in different situations.
-
Abstraction is a way to hide code behind a simpler interface. With good abstractions, you can just think about how the abstraction should act, and you don’t have to worry about how it works on the inside.
-
Testing can keep you from accidentally breaking code in one area while you’re working in another. You can assume that anything you accidentally break will be caught. This means you can focus just on the code you’re working on, and fix anything you break later.
-
Test-Driven Development will help you write code that will work the way you expected it to work. You can concentrate on writing the simplest possible implementation of your method that passes the tests. If it’s too simple and doesn’t actually work as you’d expect, your tests will catch it.
When you use these techniques in the right way, you can make some good assumptions. With these assumptions, you don’t have to keep anywhere near as much in the front of your mind. You can write better, more reliable code, because you don’t have to worry about the nearly infinite consequences your code could have on the system.
On the other hand, using these techniques in the wrong place will hide code that you actually need to see. By hiding the code, the assumptions you make will sometimes be wrong. This makes it even more likely that you’ll end up breaking it!
That’s why bad abstractions are worse than no abstraction at all, and why you can’t just throw “extract method” at code until it magically becomes good.
Right refactoring, right place
Clear code is code that’s up-front about what it’s doing. Showing what you’ll need to know is as important as hiding what you don’t need to know. When you perform a refactoring, or use a software best practice, you should think about the code you end up with, and how well it’s helped you reduce complexity. Are you helping the rest of the code make good, simplifying assumptions? Or are you burying essential knowledge behind a too-simple interface?