Deletability
After sharing out what I jokingly referred to as “Mazlow’s Trapezoid of Code Quality” the other week, several folks began adding to it. One recurring riff was that great code can be easily deleted.
“Code that easy to delete is high quality code” is a useful meme. In the ideal scenario, deleting code would be a 3 step process:
- Delete the code
- Delete its tests
- Delete the points of previous coupling, as indicated by the test suite or the compiler
Too often, though, the code is much too entangled to be able to do this in just 3 easy steps. Hidden dependencies and odd coupling points make deleting simple code difficult. Not every code base even has a clean dependency graph. Sometimes we write applications that have a dependency pile or a dependency rat’s nest.1
To optimize for deletability, we need to make sure we have a clean dependency graph. To build a clean dependency graph, we need modular code. Given that modular code is seen as a Good Thing™ in its own right, optimizing for deletability is used as a technique to achieve modular and more easily understood code.
(Note: This blog post is interested only in the mechanics of making modular or deletable code, not whether the modules created are the correct abstraction. Domain-Driven Design will help you decide whether or not the boundaries of your modules are correct.)
The Fundamental Friction
There are many different types of developers. We each value different things in the code we write.
One of the things we bikeshed over is the size of files. Some developers prefer small files, while others do not. We have two fundamental different arguments (put together succintly by Justin Duke):
Small Files are Worse
- Small files have less code
- Small files have less context
- Small files require more navigation
- Small files require more effort to work with
- Small files make it harder to understand the system
- Small files are worse
Small Files are Better
- Small files have less code
- Small files have fewer responsibilities
- Small files are simpler
- Small files are more modular
- Small files are more easily deleted
- Small files are better
You can see that to higher modularity and looser coupling are closely related to the “deletability” of a file.
How modular is too modular?
But when optimizing for modularity and deletability, we confront a contradiction of goals:
- Code should be modular
- Code should be easy to understand
Oftentimes when TDDing along, the resulting code becomes nicely modularized due to Design Pressure, the feedback that a test suite gives you as you develop. I don’t mind the modularity, but I’ve worked with people who find the modularity annoying. The argument boils down to the following: It’s hard to get a grasp of the system when you have to bounce around between many different files.2
However, I think there’s a strong correlation between this system is easy to test and this system is modular. What if being able to see everything in a single file was a bug and not a feature?
We shouldn’t try to optimize understanding of our system by localizing the total amount of data into a small number of files. Instead, we should make sure that our system is structured in a sane way. To try to understand the whole system by reading a single file is similar to reading a choose-your-own-adventure book page by page: we will get all of the information, but our brains won’t be able to figure out how it all connects.
The human brain needs a narrative, even if that narrative is told through code.
Each file should tell a simple story, for example “This file defines a class that saves the total aggregate taxes for every employee on a payroll. It gets the total tax information from an instance of TaxCalculator
class, does some stuff, and then sends it to an instance of the PayrollSaver
class.”
You can imagine that class might be nothing more than the following in Ruby:
class TaxCalculationSaver
def self.save_taxes!(payroll)
total_tax_amount = payroll.employees.map do |employee|
TaxCalculator.calculate(payroll, employee)
end.sum
PayrollSaver.save!(payroll, total_tax_amount: total_tax_amount)
end
end
This class is tiny at just 9 lines, but it does quite a bit of interesting stuff. It is the imperative shell to the TaxCalculator
functional core.3 To many, this class doesn’t seem like it should warrant its own file. But by giving it its own file, we’ve isolated its responsibilities. This will be a very simple class to test.
This will also be a very simple class to delete. Should civilization end and taxes not need to be calculated anymore, we can just delete this file and all the points where it is called. We don’t have to comb through long functions to examine each variable ending in _tax
or _total
.
By optimizing for minimal, modular classes, we are also less likely to declare bankruptcy on a file: “It’s a 500-line class, so what’s another 20 lines going to hurt?” we tell ourselves. Such a file is beyond all repair, so we might as well pile on. If modular is the default, then we never have to declare bankruptcy on these files in the code base.
Modularity and Deletability
As we modularize our code, we make our code more deletable. The more modular our code becomes, we can build a little ruleset for how safely deletable something is:
- It is easy to delete a file
- It is harder to delete a method
- It is even harder to delete a conditional branch
- It is even harder to delete a line of code
Deleting a file is a braindead operation, we just
rm app/services/tax_calculation_saver.rb
and then deal with the consequences (we delete the tests and points of coupling). Once we have to crack open a file, we must now perform careful surgery. The more specific the deletion gets, the more we hope that there is a corresponding test for the change we are about to make.
The worst feeling is deleting a chunk of code and seeing a green build. We didn’t have a test for that? Was that code even doing anything? How long has that not been working? What we want when deleting is a flip and a flop. Delete a file. Flip. Delete a test. Flop.4
By working with code, we see that modularity and deletability are closely related. Properly modularized code is easy to delete.
Writing deletable code is writing good code.
Where does your team draw the line between modularity and concision? How long is your longest file? Let me know on Twitter.
Special thanks to Justin Duke, Paul Straw, Iheanyi Ekechukwu, John McDowall, Phan Le, and Matan Zruya for reading early drafts of this post.
-
Implicit dependencies are very difficult to untangle, but it’s the unfortunate default for Rails projects. Although it can feel tedious, the explicit package includes in Python or modern JS makes it clear what a file’s dependencies are. With Rails autoloading, we implicitly create dependencies by just using a constant. ↩
-
This is assuming a Rails application, where you’re encouraged to have only 1 public class per file. Multiple public classes in a file breaks the Rails autoloading conventions, and you’re likely to have a bad time in development. ↩
-
Functional Core, Imperative Shell is a pattern explored in the “Boundaries” talk by Gary Bernhardt. It is similar to the Command Query Responsibility Segration pattern described by Martin Fowler and Greg Young. ↩
-
This flip-flop I’ve described is the Three Laws of TDD in action. It’s a careful back-and-forth between tests and code. ↩