Write code that is easy to delete, not easy to extend#
Overview#
This playfully grumpy advice comes from tef, a software engineer who has been around the block for a while. It is a mindset that is meant to help you write easily maintainable code. While I do my best to explain it here, you can also read the full post here. This hits on a key point that Sandi Metz also makes in her discussion of The Wrong Abstraction.
How did this come about?#
There is an irreducible problem that each line of code must be maintained. There is no way to get around this. One of the most logical ways to reduce maintenance without reducing the features is to write reusable code. But what happens when the requirements change? Or you couldn't forsee future requirements? Or the requirements given to you were not correct? Then you must change the code. The more reusable and extendable your code is, the harder it is to change. So in a world where your code must change, then what do you do? How do you write reusable code in a way that allows you to adapt quickly?
Be messy, clean up later#
This is a multi-step solution depending on the maturity of your codebase, and the type of code you are writing. But it boils down to: be messy, but clean up after.
Divergence from the original post
tef gives the solution as a list of steps (step 1, step 2, step 3, etc), but I like to think of them as two steps (be messy, clean up), applied to three different scenarios. So I will present them in my own formatting, but keep the titles the same so you can follow along with the original post.
This two step mindset of be messy, but clean up after can be applied in three different scenarios:
Before even diving into these scenarios, tef cracks a joke: just don't write code at all! (Step 0: Don’t write code). If somehow you can get away with this, I would also recommend it.
Function / Library code#
be messy → Step 1: Copy-paste code | clean up → Step 2: Don’t copy paste code | |
---|---|---|
What | Instead of abstracting everything away into a function or library, just copy-paste the snippets of code around. |
Wrap all the snippets of code you have copy-pasted multiple times into shared functions. Maybe put those functions into a shared library called
/utils
.
|
When | When you are just starting out, you haven’t worked through all the use cases of a specific function or library just yet. | This is more of a feeling. You have copy-pasted the snippets around a few times. It is more general code, and less application specific, like loading/saving files, etc. |
Why | It is easier to build reusable code when you have a few examples to work with, rather than trying to imagine future solutions. Shared functions are harder to change when you are just trying to get your code up and running. | Once you have copy-pasted your functions around enough times you have seen most edge cases, and its now more difficult to maintain the copy-pasted versions vs a centralized function. |
When you copy paste code around, you can easily delete one copy of it, without having to worry about the other copies. Once you know the code is stable, and not going to change, you isolate it into a function or library. This keeps the easy to delete code separate from the hard to delete code.
Boilerplate code#
be messy → Step 3: Write more boilerplate | clean up → Step 4: Don’t write boilerplate | |
---|---|---|
What | Even though you are using libraries to de-duplicate code, you are still writing a fair amount of boilerplate to interact with those libraries. Still copy paste this boilerplate around, and change the necessary components on a per process/script basis. | Wrap the flexible library with lots of boilerplate into a more opinionated library with a simpler API. This should not just be for in-house libraries, but also for external ones. |
When | As you first interact with libraries, either in-house, or external. | This is more of a feeling. You have implemented the boilerplate invocations a few times. You start to see the boundaries of all your use cases, that can be encapsulated in an API. |
Why | This is less to do with code reuse, and more to do with isolating hard-to-delete from easy-to-delete code. Library code is hard-to-delete, whereas boilerplate invocations of the library code are easy-to-delete. | This allows you make users happy, while not making things overly locked down. |
When you have a lot of boilerplate code, you are writing it in the easy to delete location. Once you know which portions of the boilerplate code are stable, you can isolate them, like you did with the library code. This keeps the easy to delete code separate from the hard to delete code.
Business logic code#
be messy → Step 5: Write a big lump of code | clean up → Step 6: Break your code into pieces | |
---|---|---|
What | While the majority of code can be handled by libraries and boilerplate code, your code still has to do something useful. The business logic code that glues all the components together should be written as one gigantic lump of gross code. | Do not break up the lumps of code into common functionality, but instead based on what it does not share with the rest of the business logic. When different modules can not be disentangled, they should be kept together. This follows the principle of loose coupling, where you can change one major component without really changing another. |
When | The first time you are writing your business logic, or when your business logic is expected to change rapidly. | After your business logic works and is running well, as it is hard to maintain a giant lump of code. It will be hard to change a component without changing everything at once. |
Why | Business logic code usually has tons of edge cases and quick and dirty hacks. If the business logic is wrong, or changes, it is easier to delete one large mess than 20 small interwoven pieces. When you know what code is going to be quickly abandoned, or easily replaced, its ok being messy. | If a module does two things, it should not be broken up into multiple components. It is easier to work with a complex module and a simple interface, rather than split the complex module and coordinate changes. |
When you business logic may change frequently as you figure out what the requirements are, you can write it in an easy to delete way. Once the logic is stable, you can isolate it by functionality, so that modules are loosely coupled. This keeps each module easy to delete, while keeping the codebase maintainable.
Also: Feature flags#
While the previous steps can be seen as a two step be messy, but clean up after, this final step is an isolated recommendation.
Step 7: Keep writing code | |
---|---|
What | Write new functionality as separate, isolated code. Do not integrate it tightly into the codebase just yet. And instead of having long-living feature branches, add feature flags to turn functionality on and off. |
When | Once you have a mature solidified codebase, and development is past the MVP. |
Why | This is less about a command line switch, but instead a way of decoupling releases from code deployment. You get a feedback loop on the feature, while it is still in development. You can also easily roll back features instantly, instead of having to re-deploy code changes. |
Summary#
Besides describing the mindset as be messy, but clean up after, it can be seen as:
- Quickly roll out a feature
- Get feedback from users
- Based on the feedback:
- Undo the changes if users do not like it
- Make the code more maintainable if they do
The key is that 1. be done in a way that is easy to undo, otherwise you will have a hard time with 3a.
And if that is too hard to remember, just remember: Do not abstract before you have to!
Criticisms#
- When development is rushed, this framework can fall apart. You either end up with developers who do the first half, and end up with a giant ball of mud, or developers who do the second half, and you get poorly abstracted, overly complex code. It requires time, effort, motivation, and leadership buy in to be able to do both.1
- It should not be seen as "easy to delete" but rather "easy to replace". The code was written in the first place because of a need. Most likely, that need will still be there, so there will just need to be a different solution.2
- Other developers will be developing on top of your code. So while you may think you can easily delete it, others may already be relying on it and it will become hard to delete.3
Other Resources#
- Write code that’s easy to delete, and easy to debug too - 2nd piece in this series of advice
- Repeat yourself, do more than one thing, and rewrite everything - 3rd piece in this series of advice
- The Wrong Abstraction - A blog post by Sandi Metz with a similar mindset of not jumping to abstractions too quickly
- Designing for the Ease of Extension and Contraction - A paper by David Parnas in a similar vein
- The Art of Destroying Software - A talk by Greg Young on the same topic
- A few Hacker News posts from February 2016, July 2020, and November 2020