I was involved in a code review at work with a couple colleagues. We were looking at a particularly tangly piece of code which had been tangly for quite some time and someone recently added some more tangle on top of it. We mused over how this kind of thing happened and what we could do to clean it up. It was obvious that the most recent tangle was added because it was impossible to understand what the code was trying to do. Rather than risk a large refactor, the author took the safer route of shoehorning the bare minimum of logic into the class to get it to “work”.
How do we “fix” this class? How do we get it where it needs to be? It was nearly impossible to fathom these questions with the problem domain obscured by layer-upon-layer of tightly interwoven control structures. I suggested that we pick a couple of easy refactors to get started. Find the pieces that have no dependencies and pull them out. We discussed it for a while and the point was raised, “Could it be dangerous to make a few, unguided, small refactors?” I’ve wondered the same thing myself. After all, you may make a small refactor in one direction but when the next person comes in to make a refactor they may make it in another direction entirely. Now your initial refactor has made this second refactor difficult because the two of you have two completely different ideas of where this code should be headed. You’ve unwittingly added to the mess.
This was a really good point which I had not given much thought to before. But I strongly believed, somewhere deep inside, that good, clean code was universal and emergent. But was this an errant belief?
There’s a general sentiment amongst the software development community that code quality is relative. The way I write my code is just as good as the way you write your code. I’m OK and you’re OK. Everyone is OK. OK? Well, not really. The legacy code that The Ancients wrote before you started at The Company and even the code you wrote a little while ago was pretty weird, right? You look at it now and can’t comprehend it. So you chalk it up to the fact that maybe you just need to become more familiar with the code base. Or maybe you know it’s bad but aren’t sure why. It’s just generally bad. But have you ever seen code written by a master? You probably have but didn’t know it. The code was simple and flowed and was easy to understand. Now that you saw the finished product, sure, you would have written it that way too, right?
That’s no accident. They didn’t happen to write the code the way that you would have. And it didn’t arrive at that state on the first go. It probably started out with the underlying algorithms expressed as nested else-ifs and strange loops that closely mirrored the language of the business requirements. The difference is that once it “worked” they didn’t close the ticket and call it a day. They took a little bit of extra time to pull the pieces apart and give names to the bigger concepts. Mixed levels of abstraction were separated. Specific behaviors were identified and elevated to the level of classes, methods, and functions.
The one thing they probably didn’t do was sit down and draft a completed design before putting code to file. So why is this? How did this work out so well? What are the odds that they didn’t write it well but in a way that was hard for most to understand?
The reason is simple: Good, clean code implies order and an ordered system can only exist in a limited number of states. There are many states that a system with high entropy can exist in. But a system with low entropy can only exist in a very constrained number of forms. Sloppy code has high entropy. Sloppy code can be written many, many different ways.
Here’s the thing, you’re never flying blind. If you can identify a SOLID principle being violated or someone breaking the Law of Demeter or poor encapsulation or mixed abstractions then let that be your guide post. Start by fixing these violations. They will also help you become familiar with the domain that you’re untangling.
So don’t worry about deciding how you want that gnarly piece of code that you unearthed to eventually look. Find the fist thing that is easy and has no dependencies and pull it off and put some tests around it. If you stop there, at least you made that piece testable. But chances are that pulling this bit of debris away will afford you a couple more that can easily be pulled away. Extract a method. Separate a mixed abstraction. Remove a singleton and inject it. Bit by bit you’ll see this code take shape and become something you and your team can admire.