I've written about the cost of abstraction before. Once you are in the IT industry for couple of decades and once you've read couple of millions lines on legacy code you become healthily suspicious of any kind of abstraction. Not that we can do without abstraction. We need it to be able to write code at all. However, each time you encounter an abstraction in the code that could have been avoided you get a little bit sadder. And some codebases are sadder than Romeo and Juliet and King Lear combined.
Remember reading an unfamiliar codebase the last time? Remember how you've thought that the authors were a bunch of incompetent idiots?
People may argue that this is because legacy stuff is necessarily convoluted, but hey, at that point you were just skimming through the codebase and you weren't understanding it deep enough to tell your typical enterprise legacy monstrosity from a work of an architectural genius. The reason you were annoyed was because you were overwhelmed by the sheer amount of unfamiliar abstraction. (To prove that, consider what was your opinion of the codebase was few months later, after getting familiar with it. It looked much better, no?)
Keep that feeling in mind. Think of it when writing new code. How will a person who doesn't know first thing about this codebase feel when reading it?
The options are not palatable. Either you try to be clever, use abstraction a lot and they'll think you are a moron. Or you get rid of all unnecessary abstraction. You'll make their life much less frustrating but they'll think you are some kind of simpleton. (And they'll probably refactor the code to make it look more clever.)
I want to give a very basic example of the phenomenon.
Imagine that the requirements are that your program does A, B, C, D and E, in that order.
You can do it in the dumbest possible way:
void main() {
// Do A.
...
// Do B.
...
// Do C.
...
// Do D.
...
// Do E.
...
}
Or maybe you notice that B, C and D are kind of related and comprise a logical unit of work:
void foo() {
// Do B.
...
// Do C.
...
// Do D.
...
}
void main() {
// Do A.
...
foo();
// Do E.
...
}
But C would probably be better off as a stand-alone function. You can imagine a case where somewhene would like to call it from elsewhere:
void bar() {
// Do C.
...
}
void foo() {
// Do B.
...
bar();
// Do D.
...
}
void main() {
// Do A.
...
foo();
// Do E.
...
}
Now think of it from the point of view of casual reader, someone who's just skimming through the code.
When they look at the first version of the code they may thing the author was a simpleton, but they can read it with ease. It looks like a story. You can read it as if it were a novel. There's nothing confusing there. The parts come in the correct order:
A
B
C
D
E
But when skimming through the refactored code that's no longer the case. What you see is:
C
B
D
A
E
It's much harder to get the grip of what's going on there but at least they'll appreciate author's cleverness.
Or maybe they won't.
January 27th, 2019
[A, B, C, D, E].map(do)
I think this is avoidable by factoring out /everything/ at once. Don't leave A and E before and after foo - they should be factored out into their own functions with self-explanatory names.
IMO your point seems right when you're just saying ABCDE and naming functions with placeholders, but when you factor out BCD the function shouldn't just be called "foo" - it's doBCD, where the name makes the purpose of the unit of work obvious.
The obvious example is some purchase calculation - imagine segments ABCDE of lookupProduct, applySitewideDiscount, applyCoupons, applyTax, processPurchase - you'd probably want to factor out all the cost calculation into some calculatePrice function, which calls applySitewideDiscount, applyCoupons, applyTax. I don't think that breaks the readability while skimming of your function, which would end up as lookupProduct, calculatePrice, processPurchase: much cleaner.
*Bad* abstraction breaks readability and flow, but good abstraction absolutely helps.
I think your example has a flow it’s not clear what goes into each of A-E (and these are bad names for functions). If they are trivial code than sure keep in one function but otherwise some guy from testing camp will say that in order to make it testable you have to split it into 5 functions. So compromise would be to try to split the code but name in a such way that function names can be read as a poem and here we come to the hardest problem in programming — naming things.
A, B, …, E are horrible function names. You shold really learn how to use proper naming.
Use proper function names which summarizes its role in the story.
Unsurprisingly, unreadable code is still unreadable even if you throw abstraction into the mix.
Indirection is not abstraction (https://hanu.la/indirection.jpg)
While indirection might be an acceptable cost for abstraction, it is not the same thing.
(Abstraction is when you find a way that several seemingly different things are the same in a meaningful way and stop worrying about the differences. Indirection is when you put several different things in boxes, but still have to care about what is inside).
I've also written academically on the coats of abstraction:
https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/toomim-linked-editing.pdf
I love this example, and the accompanying narrative!
I just want to add that there *are* some valid reasons to split up the function ‘main` into `foo` and `bar`: if foo and bar represent *design concepts* in the programmer’s mental model, then it will actually make the program *simpler* to understand by breaking them into standalone functions. However, I agree that much of the time people break a function apart without naming the parts well, and it just becomes more obtuse.
One of the arguments of the Linked Editor at the paper was that constructing proper abstractions requires thinking things trough and thinking things trough takes time and if the Linked Editor allows to avoid creating a proper abstraction, the time that it would take to construct the proper abstraction could be saved. Honestly, at the time of writing this comment here I sincerely HATE THAT APPROACH on the IDEOLOGICAL BASES: having a goal of thinking things through and designing things thoroughly WITHOUT USELESS BLOAT THAT CAN BE OPTIMIZED OUT is a matter of professional honor for me and I really do NOT want to have anything to do with colleagues or code, where sloppiness is the norm, much less if sloppiness is even a "company policy".
I also see a fundamental flaw in that "Linked Editing" approach. Namely, meaning depends on a context and one version of the cloned/multiplied function might fit to all places, where the clones are used, but the edited, next, version might be OK for one client-code region, but NOT OK for some other client-code/dependent_code region. To avoid introducing that flaw the person using the Linked Editor still has to analyse the different client-code regions that use the different linked-editable-function-clone instances. That might eliminate the time savings that the Lined Editor promised.
On the other hand, I do understand that for runtime_speed or modularization reasons duplication might be sometimes needed even after thorough thinking and careful consideration. In that case one GUI-less option might be to have the project build system incorporate a code generation step, where the "code generation" is a plain copying of function text from one central copy to the "blanks" where the clones are suppose to be. The content of the "blanks" should be overwritten every time the "code generation" build step is run. I even have a Ruby command-line tool that can be used for that(among other things): Renessaator (under BSD license):
https://github.com/martinvahi/mmmv_devel_tools/tree/master/src/mmmv_devel_tools/renessaator
Thank You for reading my comment.
This is not abstraction. This is hiding of information. Both Dependent types and Temporal Logic of Actions
use abstraction and when they do, you do not need to look at what is inside those functions or the more refined specification.
Read from page 20 where Leslie talks about the difference between abstraction and hiding.
https://archive.computerhistory.org/resources/access/text/2017/07/102717246-05-01-acc.pdf
Post preview:
Close preview