Often times when reading code written by other people, or even written by myself a sufficient amount of time ago, I fall prey to a sort of choke, a deer-in-the-headlights moment, where deleting or changing existing code just feels wrong. The code I am refactoring has been tested (hopefully), has been reviewed (hopefully), and has been running in production without error (hopefully) for long enough that no one may remember why some things work the way they do. It's natural to lock up and not want to remove a solution, despite the fact that everything is under source control (hopefully). After all, it works! If it's been working in production and it's written a certain way, there must be a reason it was written that way, even if the reason doesn't make any sense to the reader.
Right? (Wrong)
It took this longer to sink in for me than I would like to admit, but there's plenty of code that works, has been tested, has been reviewed, but that has been written the way it has, for no particular reason. In the same way that you or I might write the following concise block:
function(x){
return somethingElse(someOperation(x));
// this is obviously a contrived example
}
rather than this more descriptive block:
function(x){
var y = someOperation(x);
var z = somethingElse(y);
return z;
}
Someone else might split variable declarations, or needlessly break things into functions, or not break things into functions when they should, because they're just trying to get the job done. I've often felt guilty for prioritizing deadlines and goals over conciseness and clarity, and to an extent I should. At the end of the day though, stable, tested, working code is better than "elegant" code, clean code, or code that has been reviewed and revised to death, but that never gets published. Plus, as much as we try and optimize, test, review, and generally beat to death just about everything, some parts of software development simply come down to preference. This isn't furniture, we may not want to come back and fix it, but we can if we have to.
What happens when we inevitably do come back and fix old code? As mentioned, I personally struggle to delete or re-write old code. Not because I am too lazy, or because I don't understand it, but because I figure it has been written the way it has for a reason, with a plan, by someone more well-versed in the problem. Far too often, in my experience, that feeling is wrong.
My natural inclination towards wanting to have a name for everything left me looking for a good description of this nebulous feeling, this ascription of outsized value and explicit intent to existing code. Recently I found it thanks to a great podcast I listen to called No Cartridge.
The feeling we've been talking about aligns well with a concept called The Intentional Fallacy, which comes from literary theory. The Intentional Fallacy, as laid out by W.K. Wimsatt and Munroe Beardsley, is the idea that "the design or intention of the author is neither available nor desirable as a standard for judging the success of a work of literary art." In the context of literary criticism, their point is that the author's intent cannot be reconstructed from the text, that the piece speaks for itself and cannot be reverse-engineered to get at any sort of hidden point the author may have been trying to make, or that the written piece elides. Furthermore, they argue that even notes about a piece, or contemporaneous journals from the author at the time of the piece's writing, are still only secondary sources for deriving authorial intent, and that pieces of work as an end product are always the primary source. The text may be an imperfect medium, but it is the only one we have when we want to judge the intent, meaning, value, and impact, of a piece. These ideas may not be exactly the same when talking about code, but we can see a rhyme scheme starting to form, right?
When evaluating a piece of code, it is often impossible to derive the intent of implementation details from the code itself. That is to say, of course we can try to derive the intent of the author because we see the beginning and end states of the data, the invoked methods, etcetera, but why certain things are done a certain way can often be lost.
Comments –if they're there at all- can illuminate some of that "why", but comments are often written in the grips of code myopia, that tunnel vision that sets in after you've been plugging away at something for so long that you've lost track of the overall goal. They can also document and codify intent around an approach that is fundamentally flawed, or lend credence to an overwrought solution when a simpler one exists. After all, we're not always writing code in perfect conditions and with clear heads. Comments also carry the risk of mis-communicating authorial intent, or not being updated when the code is, leading to even worse problems downstream.
Documentation might help, but it's not a great idea to document implementation details, because it can lead to brittle documents that need to be updated as often as the code is. Documents are best used for describing the contract that a piece of code adheres to, not questions about why certain things are done a certain way. The answer might be something like "it was 2:00 in the morning, a weird bug was popping up if I did it any other way, I needed to get it done, and we never touched it again". These answers might not be best practices or something we want to settle on, but I think anyone who has worked on a production codebase has seen something like this.
The question of how to avoid these sorts of problems is better left to much more experienced developers, and there's plenty of writing on the subject. My only suggestion is to keep the concern of writing clear, clean, focused code top of mind. I've found lambda services and a service-oriented architecture, to be helpful, but no approach is ever a silver bullet.
My broader point is that code, once written, can only be questioned by its function and the reader's understanding of it. It cannot be questioned through the lens of the author's intent in any reliable way, even if you have access to the original author. Trust yourself, and if the code doesn't make sense after some effort, that's not an indictment of your intelligence. It means there's an opportunity to understand and improve a section of your codebase. If it was written a certain way on purpose, you will invariably run up against that limitation yourself. At that point you may have the opportunity to conquer that limitation, and permanently improve your team's work product. You have just as much ownership of your codebase as the original author, and you may have more of a mandate to change things if you have been expressly asked to refactor or update some particular functionality. When you do, I suggest you feel free to dig into as much of the prior implementation as you want. It'll be hard to conquer bigger and bigger problems if you're afraid of continuously playing around with functionality that seems opaque. Just remember to try and leave code clearer than it was when you came in, because someone might come to you one day asking why you implemented something a certain way, and you might realize that it really is hard to figure out the intent of an author 😄.
If you enjoyed this post, please feel free to recommend it on dev.to, share it, and give me a follow!
Also if you liked this topic and want to see a regular stream of similar stuff, or just want to get in touch, follow me on Twitter @dangolant! Also, big shoutout to my friends @thejaredwilcurt and @milkstarz on Twitter for notes.
Top comments (16)
Great line.
Was going to say the same thing.
"I figure it has been written the way it has for a reason, with a plan, by someone more well-versed in the problem. Far too often, in my experience, that feeling is wrong."
OMG THIS TOOK ME SO LONG TO GET OVER
what a great quote.
Same here, it took me seeing other people doing the same thing to realize it's kind of an absurd mental backflip.
I like to think of one aspect of this as the invisible path of implementation. It's when you get those
if (!myvar) {...}
statements - languages may vary, but there's usually several possible values that result in this being falsy. One of those cases is probably important and should have been explicit, but hey, it looks neater if we don't clog the code up with explicit checks!But, I don't hold with the idea that the original implementer did not know what they were doing and that this code should be refactored. Often, other unobvious constraints like performance, accessibility, the language itself, and the complexity inherent in the problem domain itself can dictate a certain approach that isn't immediately obvious, and creates the illegible code (maybe to the inexperienced eye) and there's no way around it without a serious technology change and/or complete rewrite. And that tends to be the way it's done, creating the cyclical lifetime of software.
And hence, I would suggest the issue is that we lack a programming language that can convey the assertions that make intention, and is still clean enough to ensure they are implemented properly. We haven't reached that evolution yet.
You are right to a certain extent.
However, I see several layers we might want to peel off before getting to a sufficiently atomic piece of an implementation to which you can apply your criticism and/or assumptions of intentionality.
The outer layers I am referring to will be related to architectural design and development guidelines, such as the adherence to specific best practices.
Whatever the project you're working on, there will already be artefacts in place that embody the architectural concepts and guidelines in use (APIs, interfaces, classes, frameworks, events, etc.), plus there will be technical requirements related to the specific technologies you are using (hardware, compilers, etc.)
When you peel off all those layers, you're left with that idiosyncratic piece of code you are talking about. But this is usually a very small chunk of code, and its peculiarities will mostly be arbitrary, or optimizations (speed/storage optimizations, compiler specific hacks, etc.), or simply stupid!
Exactly, what I am referring to is usually an incredibly small detail in the grand scheme of things.The small scope and trivial nature becomes hard to see when you're tasked with refactoring something that is considered to be a ball of mud, particularly if its a mission-critical ball of mud. I would never suggest we shouldn't consider contract-level intent, or design, just that there is a parallel for whether these sorts of details actually matter.
Right on. I think the concern (with intent) at the detailed level carries over from the concern at the higher level.
Right, figuring out what level matters or is intentional is definitely a skill!
I'm struggling with this because to me, both communicating and understanding the intent of code are necessities. That's why we give classes, methods, and variables descriptive names. Describing intent is also a purpose of unit tests.
Naturally we must at times be able to distinguish between the intended and actual behavior of code. Finding that difference and the reason for it is how we fix bugs. Without knowing intent, how would we even know if code works as expected? How could we do a code review if the coder and the reviewer couldn't communicate about the intent of the code? How could we judge whether it was well-written without knowing its intent?
OP can correct me, but this discusses code once written While in CR, the code is still flexible.
That said, I do think you're onto a point. Literature is art; code is not. This is an art to coding (i.e. "The Art of Computer Programming"), but only in the verb form of programming itself. It's not "The Art of Computer Programs".
Unlike written literature, the code we write, read, and maintain are mutable and mean to be modified for a given purpose. There are certainly critiques that can be be used across both fields, but they'll eventually be limited by the fundamental differences in the two fields.
What I got was that there are two types of optimization.
1) rewriting the implementation of a function to be faster or more efficient
2) making it so that function never has to be called in the first place.
What I think OP is trying to say is that we should be trying to do method 2 a lot often when we refactor, but it can be hard to determine why the function was written in the first place simply from the function's existence.
When I was a junior developer, I was coding for the computer. With more experience, I learned to code for other developers.
Here's a really dumbed-down illustration of how I understand intentional fallacy. After the TV show Lost ended, the creators explained in interviews details like why the characters had to repeatedly enter a series of numbers into a computer terminal. But neither that reason nor the significance of the numbers was ever explained in the show. The reason (intent) is not available within the show and cannot be determined from the work of art. Even if you knew exactly what the writers were thinking, it doesn't count.
My biggest fear with this post was that there was a connection to be drawn, but that whatever point I made would be inarticulate. I think the nuance you and Valentin touched on above is really important to get across, and in trying to tie everything all together I think the post missed that (here I am, trying to explain my intent though :D). If I had another chance, I would put it this way: the intent of the code at a top level (the contract, the API, etc.) should be easily divinable and is definitely important. Down from there, code should still be as clear as possible, but the intentions of the implementation are less important than the intentions of the contract. The costs there are lower: maybe a refactor makes it less efficient or has some smaller effect, but it has less chance of breaking a downstream piece of code if you change some implementation detail. As the choices get more granular, the more likely it is that the intent behind the choice isn't relevant (or maybe doesn't exist). Yes, for sure, at a certain level intent is clear and matters, but at the level at which I have seen friends struggle to make changes, at the implementation choices level, I think it starts to matter less (unless we're talking about a major architectural choice). That being said, I was mostly just trying to draw the connection and start a conversation, and I totally see where you're coming from.