A few years ago, I was on a project with a large, complex code base and a steady stream of feature requests from the client. The project manager had to regularly confess that, due to technical decisions we’d made earlier in the project, some feature wouldn’t be possible or would be very expensive to implement.
The situation depressed me. As a software developer, I don’t believe writing software should be like building a skyscraper, where the client has less and less freedom to change their mind as the project goes on. As construction progresses, changes become more and more expensive.
Software doesn’t have to be that way. You can and should build software to be flexible because you don’t know how you’ll need to change it.
Here are five techniques I use for writing flexible code.
1. Avoid Inheritance
The Go programming language is one of the few object-oriented languages that doesn’t offer any way for classes to inherit methods or properties from other classes. While class hierarchies are a cornerstone of object-oriented programming, they also make your code more rigid. All the classes in a common hierarchy are subtly linked, and changing any one of them could cause descendant classes to change or require changes in an ancestor. Inheritance tends to encourage code coupling and hurt your ability to make arbitrary changes later, thus reducing your code’s flexibility.
Besides creating rigid hierarchies, object inheritance also bundles together several assumptions. If a property or method is called on a class but not defined on it, the platform implicitly searches the class’s ancestors looking for a match. Customizing that behavior can be as simple as a method override or as complex as multiple inheritance and obscure design patterns. Avoiding inheritance means avoiding these sometimes complex workarounds for built-in behavior.
2. Prefer Composition
In place of inheritance, Go advises object composition. Functional programmers are familiar with function composition, and object composition is similar. Like inheritance, object composition attaches one class to another, but rather than dispatching to the attached class implicitly, object composition requires specifying exactly when and how the attached class’s methods and properties are used. It’s a minor inconvenience with powerful consequences.
Using object composition, dispatching method calls to different objects becomes trivial. You can compose multiple objects and dispatch to different ones depending on circumstances, an impossibility even with multiple inheritance. You can share common objects and state without using static properties or globals. Object composition gives you fine-grained control of the code’s behavior—an invaluable ability when you don’t know how the code will need to change in the future.
3. Don’t Abstract Incidental Similarity
We developers learn the acronym DRY at a young age: Don’t Repeat Yourself. It becomes a mantra, and we pick up a reflexive skill for spotting similar code and deduplicating it. But aggressive deduplication is often bad for flexibility, especially when the similarities are only superficial.
Take, as a parable, the case of two variables that both have the value 200. One variable specifies the maximum number of items in a list; the other is used to check whether an HTTP response was successful. Superficially, the two share their value in common, and it’s tempting to set the value of
MAX_ITEMS to another variable rather than the magic number 200. But setting
MAX_ITEMS to equal
HTTP_SUCCESS is a terrible mistake. It links two values that have nothing to do with each other. It’s better—and more flexible—to have duplicate code.
The example above may seem silly, but I’ve seen developers do that exact thing as a knee-jerk reaction to seeing two bits of code that look vaguely similar. Many times, the similarity is incidental and meaningless.
I’ve become very cautious about abstracting seeming similarities. I learned the hard way that many times, the first step to making a particular code change was undoing some abstraction. The abstraction—whether mine or someone else’s, whether old or recent—had been made with the optimistic assumption that if the many bits of abstracted code ever needed to change, they would all need to change the same way. That hasn’t been my experience; many times code changes introduce differences from shared behavior.
4. Shrink Abstractions
Abstraction is still important, and one way to avoid writing abstractions that have to be unwritten later is to keep your abstractions as small as possible. The smaller and more focused an abstraction is, the less chance there is that you’ll need to tweak it to use it in a new place; it’ll either do the right thing, or it won’t. And if you do need to tweak it but don’t want the change to apply to all the other uses of that abstraction, small abstractions are easier to write altered versions of.
A key skill for shrinking abstractions is to tease concepts apart. Rich Hickey has re-introduced the word “decomplect” into the modern vocabulary—it means “unbraid.” Often, code does several different things all together in one chunk of code. Decomplecting those responsibilities usually produces abstractions that do one very specific thing, such as setting a flag, calling a function, then unsetting the flag.
Small abstractions can also be more widely used. They’re more likely to compose well with each other. If you’re in the habit of building up small abstractions for non-trivial behavior, you’ll have a solid collection as the project matures, making it easier to introduce sophisticated behavior without writing complex code.
5. Don’t Assume
All software has assumptions baked into it, but the more assumptions you make the more rigid the code becomes. Avoiding assumptions makes your code easier to change.
Here’s an example from my current project: should such-and-such a link open in a new tab? Some supported the new tab, others opposed it, and both sides had their tradeoffs. Typically, such a debate would lead to a thorough weighing of the pros and cons, perhaps even some research, followed by choosing the apparently better option. But in this case, there was an assumption we didn’t have to make; the links don’t have to behave one way or the other, because we can configure the behavior. We now specify it with a feature flag.
While we had to add some code to allow either option, it was minor compared to the gain. Different developers can set the flag differently based on preference. If the client ever wants to turn off that behavior, it can be done without any code changes. And if some portion of users don’t like it, we can easily make it configurable per user. The app is more flexible because we avoided the assumption that it had to be one way and no other.
Stay on Track with an Accommodating Code Base
My latest project has experienced a lot of change. Requirements can come and go on a weekly basis. Using the techniques above, I’ve been able to keep up with the client’s change requests. As the project matures, the code base should become even more able to accommodate change.
If you have any other tips for writing flexible code, I’m happy to hear them.