Their second decade they learn that simplicity and readability is always better, even if it does require more code.
It's better to have 300 lines of simple, straight-forward, easy-to-read code than to have a 30 line version of the same code that's difficult to understand, and difficult to debug.
Always code for your future-self and future-others who really don't want to spend an hour figuring out what the hell you did in that awesome looking 30 lines of entropy.
Ends up with actually more documentation text than code itself, which is a good thing. But for the majority of code it an be kept simple and self-documenting.
But with a "keep it simple" philosophy the number of lines of actual code never really matters that much, while code where the developers were clever and complex at every opportunity (often just showing off) the code is quicksand and a quagmire every every single line of the way, and is a nightmare.
> If we deliberately stay away from clever techniques when writing code, in order to avoid the need for skill when debugging, we dodge the lever and miss out on the improvement. We would then need other sources of motivation in order to grow as programmers, and if no such motivation appears, our abilities stagnate (or even deteriorate).
I'm not experienced enough to guess if this is accurate, but I found it very interesting
Based on my experience, if a coworker wanted to "avoid cleverness" I imagine we'd end up arguing over something like a for-loop versus a map, but such small code decision matter very very little in comparison to overall architecture, and where you place the "seams" in your system.
So I ask, when you have felt that code is "too clever", was it because someone used a map, but you're more comfortable with for loops, or was it bigger than that?
My first job was maintaining some PHP. The author didn't know what a function was. The code was a 5,000 line script, top to bottom, with basic control and looping logic nested up to 17 deep (I counted). It was horrible. Yet surely, the author had avoided cleverness at all costs; he had built a working system with only the most basic tools, those being all he knew.
Cleverness also refers to architecture, designing meta-types to encapsulate all sorts of things that just don't need the flexibility, over-designing a system in anticipation of future needs that may never come. Sometimes it's a delicate balance and often only experience can dictate how "clever" one should be.
Many developers just out of university, or still in it, in certain languages, like to run with clever things they can do with the type system to abstract away all sorts of stuff, which becomes painful later.
- Too much business logic hidden behind dependency injection (using Dagger in this instance). I think DI should be used sparingly, to decouple large subsystems like the database or the network. It can feel clever to inject everything, so your system is super modular and decoupled, but that just makes it much harder to understand, with little or no real benefit.
- Somewhat related, excessive use of annotations to accomplish tasks that could be done in a more straightforward way with normal code. For example, in Android, you might have an annotation that adds some fragment to your activity as a mix-in; but is that really easier than just calling a function to do the same thing?
This stuff starts to cause real trouble when there are 1000 occurrences sprinkled through your code, and suddenly you need to step through it to debug a tricky problem. Straight-line code is vastly easier to deal with.
So I’d say “clever” is more of a problem at an architectural level, rather than inside individual functions.
At the low level, shorter is almost always better. If somebody comes up with a clever way to reduce a function from 10 lines to 4, say, that’s great -- as long as its purpose is clear and it’s testable.
Agreed about the annotations; it's one of the annoying things about Java, IMHO. The language itself is so inexpressive that people have to resort to annotations to do basic stuff. I think some level of "metaprogramming" can be useful (regardless of the language), either for boilerplate reduction or for removing aspects like logging from the main code, but it's too easy to become "clever" about it (not just in Java; Rubyists abuse metaprogramming way too often too, for example). I generally favour "explicit over implicit", which also means that I generally dislike inheritance because of all the non-local reasoning.
Same here! I think that approach works really well when you’re able to use it.
I’d love to try removing the DI framework and see what you get just rolling it all by hand, but that’s a tough sell in a large pre-existing project.
- handrolled is better and the framework stinks
- there are some disadvantages to the framework, but after doing it all by hand you also understand how it makes a lot of things easier
- it's a tradeoff that largely amounts to which kinds of problems you're personally more willing to put up with (quite likely outcome)
in any case you would learn something
Code that relies on implementation details of a library that a good portion of developers wouldn't necessarily know. In C# if your code relies on LINQ being lazily evaluated to produce the correct answer it's probably too clever.
Using reflection in static languages like Java or C# when it's not necessary.
Writing code that passes functions around when you don't need to.
Chaining together a series of map/filter/reduces in order to avoid writing a simple for loop.
> Based on my experience, if a coworker wanted to "avoid cleverness" I imagine we'd end up arguing over something like a for-loop versus a map, but such small code decision matter very very little in comparison to overall architecture, and where you place the "seams" in your system.
> So I ask, when you have felt that code is "too clever", was it because someone used a map, but you're more comfortable with for loops, or was it bigger than that?
This rule basically comes down to all things being equal try to write straightforward code. If you can write two functions in 5 lines of code but one function will be more easily understood by a more novice programmer, go with the more straightforward approach.
>My first job was maintaining some PHP. The author didn't know what a function was. The code was a 5,000 line script, top to bottom, with basic control and looping logic nested up to 17 deep (I counted). It was horrible. Yet surely, the author had avoided cleverness at all costs; he had built a working system with only the most basic tools, those being all he knew.
No one is arguing that writing straightforward code is the most important code quality to strive for, just that before you implement that currying solution to reduce the number of lines of code from 40 to 35. Think twice.
-I remember being called to help rewrite a few lines of Perl (the other developer didn't manage to do it): it took me two or three hours with constantly looking at a manual to rewrite these few lines in a Perl that a beginner could understand.
The end result had the same line number than the previous version..
-Configuration files made of C++ template for dubious reasons..
Instead I implemented some kind of ASN.1 tree parser, which parsed to lisp, then I tied that through C bindings to some kind of callback to the lisp functions, and clever reuse of datatypes and whatever. I don't even remember how it worked!
I don't remember if it did all the processing on the device or if I had (the same flavour) lisp running in the build system too. I just remember that in the end, even I didn't know exactly how it worked or how to hunt down the inevitable bugs. It looked very elegant though, with lots of autogenerated types from the ASN.1 spec.
The C code which eventually replaced my unholy mess had to handle the datatypes manually in each function, but it was easy to understand and easy to update. Fewer bugs too, I'm sure. And one less dependancy. (The lisp engine.) It didn't have the automatic integration with our vendor MIB file, you'd have to manually add or update functions in the C code whenever someone in another part of the company decided to update the MIB file.
Small price to pay. I'm not saying the "elegant" idea could have been made workable and easy for other developers to understand, but definitely not by me in that point in time. Lisp at the time was my hammer and I saw a lot of nails. (I wasn't even any good with the hammer for actual nail like things, I think.)
for(let i=0;i<100;)console.log((++i%3?'':'fizz')+(i%5?'':'buzz')||i)
Readable: for (let i = 1; i <= 100; i++) {
if (i % 3 == 0 && i % 5 == 0) {
console.log("FizzBuzz");
} else if (i % 3 == 0) {
console.log("Fizz");
} else if (i % 5 == 0) {
console.log("Buzz");
} else {
console.log(i);
}
} Array
.from({length: 100}, (v, k) => k+1)
.map(i => {
if (i % 3 == 0 && i % 5 == 0) return "FizzBuzz"
if (i % 3 == 0) return "Fizz"
if (i % 5 == 0) return "Buzz"
return i
})
.forEach(console.log)I spent a whole sprint building out "the RAD" - a rooted acyclic digraph - that ensured all sorts of correct behavior and powerful options for working with the graph. It felt awesome building what I thought was this super elegant thing that, to me, made total sense since I had been working with it for 3 weeks.
The other devs were confused by it and we're afraid to touch the code. I admitted failure and worked with them to rewrite it. It ended up being an adjacency list (i.e. a graph) with breadth-first traversal, but as long as I didn't call it that, they understood it and liked it.
And in hindsight, we didn't need all the guarantees that it provided, so they were right.
One I deal with at work is someone trying to be "clever" and coming up with some annotation based tool to automatically serialize and deserialize objects to a third party system. It's got complex class heirarchies, interception points, etc and I regularly have to crawl through this code. The "not clever" way would be a function that translates the objects to the very simple csv format. In fact I've done that to test interacting with it and when the bash version is simpler to read than the c# version you know it's over complicated.
Another frequent one is "this code is repeated, better move it to a function", problem is you then need to make a change and you have to go through every code path to check it's relevency or add optional parameters, then next time it needs to change you have to check all code paths and inspect which ones are using which paramters. The "not clever" way is to leave the repetitive code, this can have it's own downsides like fixing one place and missing another, but those are much easier to deal with.
Basically use the least level of abstraction that can reasonably get the job done. Your php example probably could have used a little more abstraction like functions and structures but most "enterprisy" code I see could use a lot less.
There is a great section of John Ousterhout's "A Philosophy of Software Design" that goes into this. He mentions that moving code to functions / methods doesn't eliminate complexity, it nuat kind of shifts it: it adds complexity to the "interface" of the module.
Sometimes leaving code inline with its original context is better.