It's OK if your code is just good enough
shiftmag.dev
shiftmag.dev
Yep. Especially with practice. You can pretty much get to a point where you build things reasonably well by default without even thinking too hard about it. You have to want to attain it, and be willing to ruthlessly evaluate and file down your design repeatedly.
I believe there's a compounding effect at play here, which accounts for super-linear gains in ability, given enough focus, time, maturity, and number of projects.
That's mastery. There are probably about as many master programmers as there were master... let's say blacksmiths. The problem is there are 10, 20, maybe 50 times as many programmers as we ever had journeymen blacksmiths. And they all seem to think that tenure equals mastery. If we had 10, 20, 50 times as many masters, we'd have enough people to keep an eye on things. But we don't.
Sounds more like competence
And it's often simple techniques like preferring pure functions or using immutable data structures that enable a huge improvement in maintainability, yet few seem to be employing them.
Occasionally the difference comes purely from the fact that someone looked into the library docs and chose the appropriate API method.
PR reviews don't mean throwing crap over the wall and hoping the reviewer figures it out. With this guy, there is so much back-and-forth hand holding, it would be simpler to close the PR and do it myself. But I don't...
Some of my complaints are fairly basic: test/review your own work before asking someone else to review it. This should be applicable regardless of industry.
As for the PR, don’t bother. “Hey this code you’ve submitted for code review doesn’t even compile. I’m your colleague, not a human compiler. Please don’t waste my time with this again” -> Close issue.
with two caveats:
- a HUGE disclaimer at the top saying "DO NOT MERGE: not tested"
- also, you'll probably want to politely ask someone for a review, and be specific about what you're looking for
"Draft PRs" are fine for discussing topics or code or goals with a team mate.
It's all about getting feedback!
Most teams I've seen don't practice any sort of iterative development, despite claiming to do so.
Let their manager take issue with it. Fight it, bring it to their manager's manager, whatever. A company like that is not worth working for, so try to make it a company that is, or leave, but do yourself a favor and find a team you love.
This. Remove as much ambiguity and grey area as possible.
I can not imagine having to meet everyone's personal quality bar.
You can also block commits with listing errors
There is no good code vs. bad code, there are just good programmers and bad ones.
And given how many programmers there are in total, roughly 98.76% of them are the bad ones :)
I am in my 26th year of this career and I can count on one had situations where a bad programmer wrote good code and good programmer wrote bad code.
But an even bigger problem is lack of understanding of the problem domain and a lack of documentation on how you plan to fix the problem.
Why? Because I tend to write all my code such that a complete stranger should be able to drop in and understand it. I constantly imagine that stranger looking over my shoulder while coding. I imagine the code should be maintainable and speak for itself without me there at all (I do write comments).
So, such a person SHOULD be able to change some value or logic somewhere, and rely on not having to do that anywhere else. That is the magic of local reasoning, as brought about by structured programming, after eradicating goto statements. WET code erodes that. I find it a very important principle though and value it highly.
An example where this falls apart is config files. For example, a port number might be repeated in different places. Comments are indispensable then, but they rot. So if possible, I encode it using actual language constructs.
In summary, I do err on the side of DRY rather aggressively, but don’t follow it all of the time.
and
> I usually do create what others would call unnecessary abstractions, to stay DRY.
Seem completely incompatible with
> Because I tend to write all my code such that a complete stranger should be able to drop in and understand it.
Now, I don't know the codebase you're in. It could be that your abstractions are perfectly fine, but DRY code != maintainable. You're sacrificing a ton of locality of behavior (LoB) to get that DRYness, and introducing potential spooky action at a distance. Not to mention the cognitive overhead of the abstractions.
I'm not saying to never abstract, but abstractions really only supply a benefit when the things involved are guaranteed to vary together. Usually via some sort of physical process. If it's just business logic having them vary together in the same place, eventually some dictate comes down from management to change one of them but not the other.
When this happens, you introduce weird bugs on the other side of the system. That's how a platform gets the reputation of being unmaintainable. In the bad old days, it used to be that it was globals being referenced by many different functions as well as unrestricted gotos being used to jump into the middle of a function, but I've seen it happen quite often with abstractions, even ones that seem like a good idea at the time.
The code we're talking about was actually pretty DRY. You need something that does the same thing as the last half of this function? Just push a different return address onto the stack and jump into it to re-use the code. Why repeat yourself? But it had terrible LoB, changing one function could break a completely unrelated function halfway across the project (and one that didn't even obviously call the function if you're using some sort of computed goto).
You've also identified that structured programming brought about an end to the worst of these abuses, but I think you've got the reason wrong. It's not about reducing the number of places that you need to change something. You can write perfectly structured code (actually, it's hard to write unstructured code these days) and still need to change logic in 5/10/20 places. And local reasoning is still preserved in this case, as locally would consider each of those places by themselves, assuming the logic is all in different functions/modules/etc. Structured programming changed so much because forcing functions to have defined entry/exit points allows for easier preservation of invariants. You can't have meaningful invariant checks if someone can just jump into your function just after those checks. It's also much easier to see what in the project depends on the code you're changing.
If your code isn't abstracted and WET I actually only have to look at the code currently in front of me on my screen to know fully what's happening and I can be absolutely sure that changing it won't affect anything else. True locality of thinking. Needing to use :vimgrep to update code in multiple places is smooth brain completely mechanical compared to the hell that's having to re-WET the code to split off and isolate the (potentially long) codepath that needs to change. And devs rarely put in the effort for that, more likely is they'll plumb down a flag all the way through the call stack to spooky action at a distance change the behavior of an unrelated function. Good luck figuring out that dependency later when you're starting from the lower function.
My motto has always been software is like pottery, once is DRYs it's much harder to change.
You still have to do the global analysis. You have to do that because the local code you are fixing might be a piece of business logic that has been dripped all over the code by a WET programmer. Now you fixed the logic in one place but all other places are still wrong.
The correct way to do it is to stay DRY when the reasons for changing a piece of code are going to be the same. An example would be this hypothetical business logic. If the code doesn't just look the same but is for something like business logic that needs to be the same in all 15 places it's getting applied then stay DRY. Other obvious examples are things like sorting algorithms. We banned those and put them in libraries for a reason.
This isn't an achievable goal for most complex systems. Even very well written and documented code bases (for e.g. tcmalloc, bigtable) require a good deal of background reading to develop a baseline understanding of what is going on.
I enjoy the art of programming. I love to think that, for certain types of projects, I am allowed to aim for and reach perfection.
My vision of perfection is not yours, so what. If your "good enough" is actually your perfection because of business impact, user happiness or optimal time management, good for you. Just don't tell me that my perfection does not exist.
Sometimes, it's good to know that you can do something just for the beauty of it, and programming could (should!) be one of them.
I've seen countless bright minds wonder in the pursuit of instant pleasure by adding unnecessary complexity. I have seen others outright sacrificing projects that support people's life to achieve an instant goal of learning a particular library or acquire a useful skill or worse make a point against an imaginary adversary.
Due to the incompetent management, these suckers are never punished. They usually jump board and venture into greener pastures before their playgrounds turns into bloody combat fields where much less sophisticated but more honest former colleagues die or deliver.
So I can not share this experience.
Or this week, Saturday.
Context and priority should be defined for a project, not just left to the decision of each developer.
I am building code for a startup right now. The context and priority is to "get the damn thing working". Thus code quality is largely irrelevant - this codebase is flat out garbage - it is full of commented out code, duplication, files that were obviated ages ago. It is unstructured, disorganised, uses different approaches to solving the same problem all over the place - there are ZERO tests, no CI/CD, the code is uploaded directly to production. This is exactly the right way to build this because none of those "terrible sins" matter when you have no customers and your only goal is to get something working as fast as possible and every secong spent making things nice is a waste of time and money because if the business fails then every second spent making things nice was wasted.
If however I was working for Nasa on code that was running a rocket launch system, then hopefully it is stated to all programmers working on the system that reliability is priority one. This informs every about how the code is written from that point. It means few lines of code, alot more eyes on the code, must more rigorous quality control and much lower overall output.
If however I was working in an ordinary business making a CRM system then the stated priority I imagine would be something like "we want a balance between productivity, reliability, maintainability" etc. This explicit definition of the context and priority sets the scene for how the code will be written.
I've never worked anywhere that is was explicitly stated across a range of parameters what the code should prioritise in terms of security/reliability/performance/maintainability/time to market/quality etc.
There's a lot of truth to this, but I would also like to see companies, and possible even individuals in the most negligent cases, be held liable for damages that come to customers when security breaches happen.
We wouldn't build a bridge with that attitude: "Just scribble whatever on those plans! We need to get this thing built right now! None of this matters if the bridge doesn't exist and people aren't drive across it!" For the same reasons we wouldn't do this with a bridge, we shouldn't do this with software, although to a lesser extent.
Again, that must be defined as context and priority.
Priorities:
* deliver as fast as possible
* security matters
These two priorities are somewhat in conflict but it's still important to state them, then developers know where to focus.
I'd also add that in contexts where there are hard safety/quality/reliability requirements, having individuals or teams or project team leads responsible for a bunch of conflicting objectives and constraints often produces situations where there will be pressure to make visible short-term progress, at the expense of increasing longer-term risks of issues that are less visible and difficult to measure.
> If however I was working for Nasa on code that was running a rocket launch system, then hopefully it is stated to all programmers working on the system that reliability is priority one.
To avoid this, responsibilities should be structured so there's a different team responsible for QA whose review and approval is necessary to proceed for go / no-go decisions. The QA team responsible for the QA function should be independent from and isolated from the delivery team doing the work, and the pressures and influence of the delivery team's management chain.
In contexts where there's a lot of potential for harm, ideally the QA team shouldn't even be part of the same org (e.g. QA by an external public-sector industry regulator with the power to withhold or retract licenses to do business from companies that cannot demonstrate their products and services are safe), to make the QA team less susceptible to pressure.
Your users don't care how clean your source code is, but they definitely care if it's slow and buggy.
They do care about bugs and new features though, and bad code quality will lead you to more bugs and slower shipping of features in the medium/long run. At least, that's how I define good code.
That's also pretty much the only hope of getting product buy-in on this kind of thing, showing that "no really, there was this particular customer impact". Or "by doing this upgrade in a timely manner, we're not screwed by no longer being able to buy old hardware because new kernels/libraries support the modern replacements"...
The customers are unhappy. I am unhappy. Everyone is unhappy.
I prefer "messy" simple code that works and is easy to understand. Yeah sure I put too much logic into the controller. I could put it into a dozen different files. Fight me.
Copying and pasting a line of code can become a future bug when someone doesn't refactor a line of code. But building some overwrought framework to avoid ever repeating anything can introduce classes of bugs that are much much harder to solve because of the layers of indirection.
And depending on what vertical you're in, sometimes releasing something is the best path to a more reliable app. If you're writing crud apps where 99% of the complexity is defined by business rules, getting a V1 out so you can learn all the places where the business rules were improperly captured gets you a better result than bikeshedding endlessly about having the most polished version of those broken assumptions.
tl;dr: sometimes "worse code" leads to a legitimately better end product for your users.
The development of an engineer:
1. newbie - follow the rules because you're told to
2. master - follow the rules because you understand them
3. guru - break the rules because your knowledge transcends them
I often see code that follows the rules right into a swamp. Your example is a fine illustration of this.
If the thing being written is not going to be updated at all, then, sure, quality is not important.
Yeah they “got it done” but we spend 80% of our time fighting fires and the 20% left on new development takes ten times longer than it ought to because zero thought or care was put into anything other than “it works for me”.
This to me is the difference between engineers and programmers. Programmers can get something done and out the door, but engineers can build something that is easy to iterate on and easy to reason about.
I mean, then they weren't good engineers? Nobody said that approach is good.
But I've also seen enough for my share of engineers that knowingly write buggy code that eventually blows up in someone's face because that code was simpler and turned out elegant that way. Code simplicity and reality don't always go hand in hand. The startup graveyard is filled with businesses with otherwise great engineers that lost sight of the customer's actual experience.
In my experience, that happens way more often than teams failing to produce value because they’ve spent eons polishing something to perfection.
On the other hand, our industry’s culture of not taking the time for anything to be built a little better means we have an enormous number of seemingly-experienced engineers who lack the understanding of how to write well-built software even if they are given the time. Which leads to individuals concluding that time spent cleaning things up is a waste because they end up with something worse and more complicated afterward. So they don’t invest in learning this skill, and the cycle repeats.
I wish we had someone doing that with code.
It's like the fat acceptance movement "it's OK if you're plus sized, or plus plus plus sized, or I guess multiply exponent factorial sized".
But it's really not OK. Not just apps but even our operating systems have turned into layer upon layer of unfinished and half forgotten features that sum up into something literally worse than what random natural selection wrote in our DNA. That's right. Random chance, throwing crap at the wall is better than our "software engineering".
Be better.
And it is this complexity which drags down performance as well. If a smartphone app is nothing else than a glorified web browser showing some heavy javascript riddled abonimation you don't need to wonder why the things are sluggish and memory hogs to boot. Not all apps are like that, but you get the idea.
But to lighten the mood a bit: https://www.youtube.com/watch?v=gWVmPtr9O0g (Titan 2 demo on SEGA Mega Drive / Genesis)
If we peg 3 as "good enough to ship and stand behind it", then I'm immediately thinking about getting the code to somewhere in the 3.5 range. After you ship, you're in a good position to revisit some of the decisions you mode. Everything's fresh, and you can go in and squash some bugs that you know are in there, expand some tests that you knew weren't as thorough as you wanted, and trim some of the crap that you realize that you don't need. Maybe it's time to do some refactoring, now that you have the big picture. Maybe it's time to chase after some performance improvements. Maybe it's time to make the integration tests faster and better.
- make it
- make it work
- make it fast
Make it right
Make it fast
POC-quality code that doesn't have clean boundaries and is tightly coupled isn't necessarily a problem if it is an internal detail of some application or library that is cheap to change in future if necessary, and its impact is localized. As long as it works, if there aren't any forces that cause it to be revisited, maybe it can be left to be low-quality forever, without any further impact.
Where things get concerning are if the cost and coordination required to change the design in future grows over time or becomes effectively impossible. E.g. if the POC-quality stuff ends up being propagated internally throughout the codebase over time as developers make changes and add it into more and more places -- maybe its within the control of a single team to fix it, but if left unchecked the effort grows from a few hours work in once place to something requiring planning, systemic refactoring, testing, dedicated effort over a period of months.
Or, worse, if the POC-quality poor design has ended up polluting system interfaces between components owned by multiple teams or multiple organizations, so removing it would become a multi-month or multi-year coordination process between groups of people with different priorities, requiring a V2 release, deprecation of V1 & migration.
Here's another person making a dangerous analogy between code and a goal with a fixed end date. A paper that has been graded is done. A book that has been published is 99.9% done.
Code that is no longer being touched is not done; it's dead.
I have a five year plan for every tree in my yard. You can't rewrite trees, and there's a maximum rate at which you can refactor them. So there's what you can do now, what you will do next, and everything beyond that is educated speculation. You can't control it. You can't control the elements or disease or accidents.
So I know what I want to do, and I know how much I will do in the spring, and how much I'll have to delay until next year. And next year, or the year after, I'll step back, look at the whole thing again, and make a new plan, that might not look too much like my current plan. It all depends on what the other forces acting on my projects get up to in the meantime.
Like the trees, you can't control your coworkers, you can only influence, steer and remove. If you try to exert more control, you end up with a tiny little tree. And the dirty little secret with those is that the tree still does largely what it wants, and the skill is in making what actually happened look like it was on purpose.
If you want a big happy tree, you have to focus on the irreversible decisions, and let a lot of the little shit go (for now), and sometimes try again later. If all goes well, the only person who thinks the end result is a mess will be you. A layperson will think it was all going according to your plan.
Scripting code I wrote in 1998 worked in 2005 and still works today. Javascript I wrote 5 years ago, works today. Language choice matters as much as how it's executed. I assume VMware running a vm from 2008 is still running somewhere.
If it's not being executed, it's dead. There's a big difference from the "always needs to be maintained" assumption.
The goal should be to write code not needing maintenance.
Four weeks ago I contacted a coworker to ask about some routines he wrote 5 years ago. He said he hadn't touched them in 5 years. The code has been tested continuously in the interim. His old code worked perfectly for me the first time and it saved me hours.
It’s basically abandonware that is waiting for one major problem to render it obsolete. I don’t entirely agree with npm and GitHub ranking projects by recent activity, but they’re not entirely wrong either.
You can always be clarifying variable names or shoring up docs. Updating dependencies and keeping track of APIs without necessarily changing the fundamentals of the project.
In other projects, the domain is clearer, or the system already has well defined patterns that should be followed. In this mode fast iteration is also possible, but it's because the code is clean and follows strong patterns making it easy to understand. Good Enough code here is quite likely to slow the team down as they grapple with needless bugs and code that's hard to decompose / refactor.
The most important aspect of quality is that the team defines the level of quality that's needed for the project or the work being undertaken, and they deliver to that. Have the conversation up front about what level of quality to aim for and why. Then the team is on the same page, and everyone can move forward with the same expectations.
The internal facing problem is getting a team to agree to differing quality gates for different parts of the system - the absolutely knowable and the arguably unknowable parts should not be written with the same mindset if you want to maintain velocity. If you get lucky with the org chart you can fake some of that quality diversity via code ownership, but that's a rough approximation at best. People seem to prefer picking something static and not thinking about it too much, rather than having to reason about every feature. I'm curious to see what we try next to deal with this.
There are some things that genuinely matter such as minimizing repetition, using variable names that are clear/easily searchable with "find" (meaning without tons of false positives) and not writing undebuggable code if you can avoid it[0]. I also think performance matters even if it seems fast enough on your machine. In my view, you shouldn't use Integer instead of int in Java unless you absolutely have to because Integer wastes resources creating an object containing an int and dramatically increases cache misses[1]. But in general, it isn't worth worrying about unless you can actually come up with a coherent explanation of why your preferred way of writing code will make the software perform better or be easier to maintain. Of course, the only absolute rule in code is that there's always an exception to every rule.
[0]: I'm generally in the "C/C++ macros considered harmful" camp especially when they resemble functions and feel similarly about anything else that makes the code execution path less than straightforward to follow.
[1]: I have a strong suspicion that OOP itself is an anti-pattern and that the entire paradigm is a wrong turn that needs to be abandoned. It's weird because I had a favorable opinion of OOP before I learned what it is in college but it tripped my brain's BS alarm. But I've never worked in a large enterprise environment so I haven't actually seen it in practice enough to fairly evaluate it.
I've worked with a few too many instances of code golf where the resulting code requires too many brain cells to comprehend. If I wanted to dedicate 5% of my attention to 50 different libraries, I'd need 3 more brains to do it, but most libraries are written that way. Some seem to think they're entitled to 10%. More.
Show me a library that's a snoozefest to figure out why I put it 5 and got out false when I expected true. That's the one I want to use.
Your code is a distillation of how well you understand the problem and how it's being solved. Confusion usually means either the requirements are not well-understood, you still have unknowns, or you simply don't understand the problem/solution well enough to express it to both humans and the computer fluently. All of those involve thinking more and getting more information.
Really, I write the best code I can given the circumstances so I don't have to keep coming back to the same section of code over and over. I want to solve it as well as necessary and move onto something new.
Also, why is the tech industry so weird in how it continually feels the need to degrade the importance of technical skills? Is it seen as taboo that there are still large differences in individual programmer skill?
You need huge numbers of average developers to keep running all the software there is.
Just like in army, average Joe can be a soldier because there will never be enough “best of the best” to have an army of only special forces.
For something to be "good enough" it still has to be good. This feels like evil propaganda aimed at the poor souls who work for cash-strapped and inexperienced entrepreneurs.
Implementing a feature fast is no excuse for writing crappy code.
There are many sets of constraints to satisfy when you're writing code. I agree chasing "perfection" is pointless, but too often you see inexperienced people rationalizing their shoddy work. If you're excusing yourself from bothering with crazy optimizations that have little to no business impact, fine it's good enough. If you're excusing spaghetti, you're the inexperienced person I'm talking about. The "good enough" example from the article sounds like spaghetti.
That looks like an absolute nightmare and not an example of very good code.
The closest thing we have is "does this code do what the user wants it to do". To me, this is the only question that really matters.
There's a lot of factory code stuff which don't convey any information and very long chains of folders which does not help comprehension
https://github.com/infobip/infobip-spring-data-querydsl/blob...
It was a single huge function, called from cron every 5 minutes. No locking to prevent concurrent runs if it took longer than five minutes to execute. No exception handling. One giant nearly incomprehensible everything-function. Global variables. Bugs everywhere.
Easily hundreds of thousands of dollars of net profit per hour (some hours).
Since then I never worry much about code quality in my prototypes. Build one to throw away.
In general, I agree. I don’t write code that bad, even for prototypes. That said, I worry a lot less about being super meticulous DRY and best practices in my prototypes that in 90% of cases will never touch millions in value. Done is better than perfect.
All software is not life or death. But software can be something people come to rely on.
If I choose (unknowingly) to rely on software not done well and it bites me, I personally would rather not have relied on it at all.
However, I may be a tad famous about striving for perfection in my code. [1] [2]
Why? If "good enough" is good enough, why do I go further?
For a few reasons:
1. I want the industry to be more professional [3] where it matters, and I need to set an example.
2. The kind of software I write already has alternatives, so mine needs to be far better to get adopted. And it does. [4]
3. Also, I am just a perfectionist. It's a problem.
Anyway, "good enough" is good enough most of the time; just make sure that your situation doesn't require more.
[1]: https://gavinhoward.com/2019/08/why-perfect-software-is-near...
[2]: https://git.gavinhoward.com/gavin/bc/src/commit/22253a3a6/ME...
[3]: https://gavinhoward.com/2022/10/we-must-professionalize-prog...
[4]: https://gavinhoward.com/2023/02/my-code-conquered-another-os...
All of the options for doing so in C are awful; I just think goto is the least bad option. Otherwise, you get if statements that keep nesting, deeper and deeper.
And what's the hacky-ish logic you're talking about?
If a developer can write error-free binary code that improves performance (as seen by the user) by 0.1%, BUT the next developer (or even the same dev months later) can't adjust the code without all hell breaking loose, then that code is basically awful.
Side note: add your newline at the end of your files before commit! Ugh
the typical collaborative implementation environment is a disaster. we are baking a cake, slowly over weeks and months. we aren’t sure why or who’s at fault, but we are absolutely sure it looks awful and tastes worse.
the only silver lining is that the solution to this disaster is hiring more collaborators. jobs and ubi all around.
microservices obviously didn’t quite work, but were an idea in the right direction. we need to collaborate at a higher level than code. we need to work in a bakery together, but each bake alone.
then we can easily evaluate the quality and pace of each other. there is no ambiguity of individual responsibility.
when my cake is bad, i should feel bad. i should look around the kitchen for better cakes, and ask their baker what they do that i don’t.
when my cake is bad and i don’t care, my boss should move me to less important cakes, or out of baking all together.
The masterpiece I fretted over for endless hours is guaranteed to be obsolete within 1 year.
The crappy hack with the comment that says "@TODO make not be garbage sorry" is cursed to live on for eternity.
I think good code is vital to the software development process. It doesn't have to start out good but it should end good. Because you're going to be back at this code over and over. A little bit of effort up front can save you a lot of time in the long term.
Two, I don't think 4 is necessarily more effort than 3 (for some values of 4 and 3). What does take a lot of time is if different engineers have different ideas of what 3 and 4 are but lack the perspective to understand each other and choose a common standard. Everyone will choose faster if everybody follows static typing because you can rely on assumptions you otherwise couldn't. And, everyone can move faster if we don't worry about any of that static typing crap. If engineers take different approaches, everybody will move slower and probably hate their jobs as well.
And it's not even just different among people. Along the time dimension your perfect code can become bad later as requirements change and things evolve you may realize that what was once (in your opinion) perfect code was actually a very bad way to incorporate a certain feature.
Think of perfect code as a controversial literature novel. There is literally no point in building perfection unless your goal is only to build perfection for yourself rather then a customer/audience.
Getting things done is more important. Excluding above scenario, either you will make mistakes, or you are not tackling meaningful tasks. And that's okay. Allocate time for clean up when there's less ambiguity. The more you explore the problem, the better the issues become.
First implementation will always be bad, so throw it away and build something that's good enough to get the job done and uses what you've learned as you explored the space.
This is a poorly considered sentiment.
It sounds like a steaming pile of garbage to me.
2) Make it right.
3) Make it fast.
Meanwhile if you 2) 3) 1), you get cut off around step 2.99 and everyone's life sucks less.
don’t get caught up in dogma and ideology in search of the “right” answer. Don’t be a “fanboy”. There is no such thing as the “right” or “best” solution because every engineering decision has tradeoffs.
You have to make rational, pragmatic, decisions based on the facts on the facts on the ground.