Smart Programmers Write STUPID Code (2022)
bdicroce.medium.com
bdicroce.medium.com
Does all this "snake-oil" actually work? My decades of personal experience says otherwise. You can't teach surface-level rote memorisation crap like this and expect it to make things better. I agree that simplicity is good and accidental complexity is to be avoided, but it's not something that can be distilled into trite talking points to be taught.
"There's no replacement for intelligence."
Good judgement mostly seems to come from bad judgement, failure, then picking the pieces up again while evaluating the causes. It also seems to require some emotional maturity and humility.
You are correct that the various dogmas are not useful until the mental attitude is right. After then they are superfluous.
Un-wise super intelligent programmers are the worst. They come up with the most amazing roundabout way to achieve even the most mundane tasks, and consider complex code a mark of honour, rather than a pathology to be purged.
At some point people mature and realize that their job when writing code for work is not to concoct brilliant puzzles for next maintainer, but to do exactly the opposite. And then they reach the meta-level of realizing that simplifying things is actually just as hard and pleasing as puzzle-making, and they've reached a maturity in the quality their program design.
The worst kinds of programmers are those that plateau at the "Riddler" stage. They potentially are able carve themselves a nice safe niche, but the value they could have delivered could have been so much more if they'd ever matured. "Oh, that code... yeah, better call Edward ... it's his baby, supercomplex. Nobody seems to be smart enough to work in it except Mr. Nygma".
There are pieces of code that are potentially super-complex due to the intrinsic complexity of the problem space. But since for an outsider it's impossible to distinguish between the two, utmost empathy should be always used to introduce new people to such a thing.
It's like trying to solve a puzzle that doesn't have a solution.
No, "simplicity is good and accidental complexity is to be avoided" is that distillation. The difficulty is less the "what" than the "how". Compressing experience, expertise, wisdom, taste, humility, judgement and courage into an acronym is always going to fall short. Another useless dogma as you say.
DRY, KISS, SOLID etc.
Yeah they’re buzzwords, but they serve a pretty useful function of constantly reinforcing objectively good behaviours that should be inherently obvious but often are not.
That said, my argument falls apart with DevOps which is simply moving automation to more stages of your delivery (which is good) because it’s been utterly co-opted by snake oil salesmen and uppity gatekeepers. Seriously, the amount of exceedingly difficult but otherwise passable workers I’ve had to straighten out because they’ve mistaken having access control rights to work with the production environment with being in charge of the whole development process is utterly wild.
(I’ve checked too, no-one has told them or lead them to believe this so I’m sure it’s A DevOps thing).
And, you know, Agile. But it’s not an acronym so I’ll leave that rant out.
Yes, but that said, if there’s gonna be a dogma I think this one is pretty good. To me, software engineering is the art of curbing complexity.
I can see a couple of missteps:
- simplicity can be confused with the appearance of simplicity. Yaml is a great example of this phenomenon.
- simplicity is subjective, and should ideally be measured by outsiders. However, as we all know decisions and moat belong mostly to insiders, senior engineers that often want to flex and build moat. There’s a risk of curse of knowledge because you can just say “this is simple to me (and if you don’t get it you’re stupid)”
- when more people can understand something, you can get more chefs in the kitchen. This is not a problem with simplicity but can be a problem of governance.
- engineers can move visible complexity around to areas of less scrutiny. For instance, if there’s a “rule” that a module should be simple, you can break it into two and say they are each simple (even if they’re coupled and it’s only a cosmetic modularization)
- complexity from dependencies, concurrency, memory usage etc can fly under the radar, especially for less experienced/gung ho engineers.
To be clear, I still like this new trend, these are just some immediate issues that come to mind.
There also existed many interfaces with exactly one implementation, which just increases the cognitive load when writing code because you see an 'IDoX' interface only to discover it is ALWAYS an 'XAdapater' that if you had known, would have saved mountains of logic (ie, getting a new 'XAdapater' from the container while copying some stuff from the 'IDoX' interface). It was a mess.
Are they going to be glad that all these abstractions exist at some point? Probably not. This is a pretty simple, but mature project. There's no real reason to refactor it at this point. The business owners just wanted to know what was going on and why simple changes took months to implement.
It still can make sense if you've got a very complex bundle of isolated stateless business logic but in practice I find that most dependency inversion I see these days isn't for that. It increases the SLOC by 30% and reduces code coherence just so that a test can run in 20 milliseconds instead of 2.1 seconds.
In many cases those unit tests check to see if you put a number in one end of a class that the same number comes out the other end. What bug is that going to catch?
Meanwhile all of the actual bugs are probably hiding in the database queries or the interaction layers created as a sacrifice to bob the bearded god of unit testing.
The most fascinating instantiation of this idea is, I think, the notion that because unit tests are horrible to work with that unit tests can therefore "drive" design because it'll make you fight to make the unit test less horrible. It's like advocating an abusive relationship to help fix your life because the abuser won't be afraid to point out all of your flaws which you can then fix. The two corollaries that never seems to permeate are A) maybe when unit tests become horrible to work it's means they were also the bad code that coupled too tightly? and B) maybe it's cheaper to spot bad design by learning what to look for rather than building a unit test that will punch you in the face for it?
I once had a bug scenario I wanted to reproduce with a test where the time was grabbed from datetime.now() in the first half of the scenario and indirectly via a postgres query in the second half (with a time delimited query).
With libfaketime I injected a new running service time and postgres service via the test runner. It let me do TDD to create a reproducible test - what you thought was impossible.
I could have jumped straight to dependency injection on that mess of a code base but that would have meant a risky architectural change that I would have to have done without tests. That is bad practice.
This sort of thing was not really possible in 2001 but it's possible now.
There are people who cargo cult and people who do things for a reason. It may not always be obvious which category a developer was in just by talking to them or looking at their work.
Imagine the following scenario: you have two projects each having the same functionality and the same level of test coverage. Project A is 30% longer than project B but project A's test suite runs in 1 second while project B's runs in 2 minutes. Personally, I am taking project A every time (but I'm also probably trying to figure out how to get that 30% down to 0.1-2%).
The path to project B is paved with 50 decisions to do the thing that took 2.1 seconds rather than 20ms.
Project A's tests will usually be sacrificing realism in favor of speed. The practical upshot of that is entire classes of bug won't get caught.
To take some recent practical examples off the top of my head: I have some tests that do 404 checks on all web pages. I have another that checks if sent emails are outlook compliant. These checks necessitate that the tests be slower but somebody still needs to do this.
I know there are plenty of people out there who think like you do. That was kind of my point. Realism in testing is an underappreciated quality.
I decided to follow the «Wrong abstraction is worse than duplication» approach. Doing this for 5+ years now and did never regret it.
Sure, copy-pasta sucks but in comparison to over engineering it’s an acceptable price to pay.
“Cognitive load refers to the amount of effort that is exerted or required while reasoning and thinking. Any mental process, from memory to perception to language, creates a cognitive load because it requires energy and effort. When cognitive load is high, thought processes are potentially interfered with. To the UX designer, a common goal when designing interfaces would be to keep users’ cognitive load to a minimum.”
(https://speakerdeck.com/fedepaol/reducing-cognitive-load-yet...)
In this context, "users" can be substituted with "other developers". Writing low-CL code is a constant balancing act and, like good UX design, when it works the nuances are invisible and therefore hard to learn from.
So I appreciate the OP for the article even if we can quibble about some of the points.
With cognitive load it doesn't even generalize. Who hasn't worked with somebody who joined a project and then immediately started changing the code to be more aligned to their taste in idioms to reduce the cognitive load. Their cognitive load that is. Not necessarily anybody else's.
My point, the unfamiliar might have actually lower cognitive load. Cognitive load is a function of what you know and what you are used to.
Altogether, some OK ideas, but quite generic and insufficient/incomplete. Coupled with the conversational marketing style, on the whole, frustrating.
Keep thinking and writing.
Other implications include naming conventions, sufficient amount of documentation, what architectural patterns to choose (if you need to do something that even hints of accidental complexity you need to have really good justification for it)...etc.
Go is your friend.
https://www.lihaoyi.com/post/StrategicScalaStylePrincipleofL...
The code itself should be treated internally like part of the product and it should be easily extensible, of uniform style, and written so safely that if a child added a line to the code it wouldn't break it entirely.
This is especially true of languages that support macros. If inclusion or modification of macros in your code cannot be done, it is probably a bad sign. When writing code to be resistant to hardware attacks for instance there is a different style that must be adopted entirely, if everyone is writing the fanciest for-loops fathomable then it becomes inefficient and a risk to code correctness to mutate the code to resist classes of attacks. This is terrible and not worth it just because an elite haxor wanted to write a for loop in a single line.
I've worked on codebases where omitting curly braces for single line IF was the style, but can't recall ever being 'bitten in the ass' by that particular style choice. I can see how when combined with other questionable style choices it could yield some very ugly and error prone code, but by itself I really have trouble understanding why people object to it.
What a treasure of an article. Definitely felt the leveling up happening as I was reading.
You don't write code just to please the compiler, but it's a big deal. (Obviously, if you just wanted to please the compiler, it wouldn't have to be indented, and your variables could have names like l0327.)
A sufficiently smart compiler could warn you that you're introducing an object X with the same meaning as object Y, so your coworker can sleep at night. Or if not compiler than some other tool. We can imagine tooling that scans the code base and tells you that a class exists that is very similar to what you just wrote.
A compiler was written by your de facto coworkers, likely in another organization far away. When their ball-busting attempts turn up nothing against your code, that is a kind of praise.
Developers should take care to elicit this language from their colleagues and use it in their code instead of using whatever terms come to their mind when they need to name things.
- SOLID
- KISS
- YAGNI
- DRY
- ...
I think article is a bit naive. If your system is designed this way, you probably have no control over it. Maybe some legacy stuff, maybe you jump too fast between 300 microservices...
It hat case you need other methods, like very good logging. If you can replay every interaction with that service, you do not have to decouple it!
Personally I have no issue just figured I would relay my impulsive "news aggregation junkie" thoughts.
"STUPID: A mnemonic for great code" might be less impulse triggering and be more forthright. But you have to bring them in somehow so meh.