Hacker News new | ask | show | jobs
by josephg 1780 days ago
This is a hot take but: I have a growing sense that one defining feature of some software engineers is that they’re embarrassed by dense logic. I think you see this in the Java world where people seem to hide the core logic of their program amidst a dizzying array of interfaces and deep function call chains. Maybe with enough DI and whatnot, the business logic itself can melt into the structure of the program.

In comparison, C programs tend not to hide this stuff. C functions are often long and complex. If you were implementing quicksort in C, you would write (more or less) one function with all the logic packed in there you can just read top to bottom. In Java it would be a nest of SortComparator interfaces and SortAlgorithm implementors, which would act to hide the algorithm itself.

There’s something more honest about the C style. It’s like, yeah, the algorithm is complicated. So we put it all together in one dense function. Here it is - go nuts! You don’t have to go hunting for the right implementing class. Or divine how FooFactory has configured your Foo object instance.

All that Java style class abstraction seems to (intentionally or otherwise) make the actual logic of your program hard to find and hard to trace. It’s coy. When I’m trying to read someone’s code, that’s simply never what I want.

7 comments

Personally, I don't think it has anything to do with being embarrassed about dense logic. I think it's about a love of abstractions.

Many developers get trapped trying to recognize patterns and come up with perfect mental models for whatever problem they're trying to solve when the straight forward dense function is probably the simpler and more maintainable solution. I often fall for this trap myself and am constantly trying to be wary of it.

> In Java it would be a nest of SortComparator interfaces and SortAlgorithm implementors, which would act to hide the algorithm itself.

That is odd because when I look at the Java style, DualPivotQuicksort [1], it seems that the language authors did not do that. This is so very strange! Their methods are long, complex, and highly documented. Maybe they must be really incompetent Java programmers? I mean, they did put this obtuse abstraction by hiding it in Arrays. That's super crazy, just look at this monstrosity!!!

    public static void sort(int[] a) {
      DualPivotQuicksort.sort(a, 0, 0, a.length);
    }
Maybe you need to have an intervention with them? Somehow, in some crazy world, even Java programmers are capable of writing good and efficient code. Its like bad developers might do an awful in job in whatever language they used? Can't be true...

[1] https://github.com/openjdk/jdk/blob/master/src/java.base/sha...

The "enterprise" programming culture which has emerged around Java doesn't impinge on all java code. But it is still a real thing. The fact that openjdk authors can rise above it and write clean, reusable Java code is obviously good. The fact that there is something that needs to be risen above is unfortunate.

I think the same about Javascript, though the specifics are different. I've been writing JS for years, but I've been moving away from it lately because increasingly I feel like an odd duck in the JS world. Most javascript programmers have much less experience (in any language) than I do. When I mention I write a lot of javascript professionally, people assume I'm an fresh faced bootcamp grad. Its sometimes hard to find high quality libraries on npm because the average quality there is reasonably low. Eg good luck finding a password generator which doesn't use Math.random(). Or finding an email parsing library which preserves the order of email headers. (This has semantic content!)

Does there exist high quality java + javascript code? Sure. But even macdonalds makes good food sometimes. That isn't enough to make me a regular customer.

Sometimes the right call is to fight for the ecosystem you're a part of and help it improve. I've done a lot of that. But you don't have to fight for high quality software if you just go where the high quality software is being made. Its easier to switch languages than change a culture.

... I'm being a bit sloppy and judgemental here. Maybe it would be better to say, each ecosystem has a set of values. Javascript values programming velocity, accessibility to new programmers and simplicity. Java has a different list. Insomuch as you're living inside an ecosystem, you don't get to simply ignore and dismiss those influences when you don't like them. It sucks writing Go if you hate gofmt. It sucks writing rust if you don't even want the borrow checker. And it sucks writing Java if you hate dealing with AbstractIteratorFactoryImpl. Even if there's some redeeming code in openjdk.

The "enterprise" programming culture did not emerge around Java.

It was already been there regardless of the language.

I am old enough to have done Enterprise C, Enterprise C++, Enterprise VB, Enterprise Delphi, Enterprise xBase, ...

I've seen and worked with enterprise golang code. Ugh.
A language's core values is not determined by a small, vocal, subculture of its ecosystem. In Java's case, it was set early on to be a "blue collar" language [1]. If programmers fail to follow the spirit of the language then it will be awkward.

"So, how does Java feel? Java feels playful and flexible. You can build things with it that are themselves flexible. Java feels deterministic. You feel like it’s going to do what you ask it to do. It feels fairly nonthreatening in that you can just try something and you’ll quickly get an error message if it’s crazy. It feels pretty rich. We tried hard to have a fairly large class library straight out of the box. By and large, it feels like you can just sit down and write code."

Please do not push the programming community towards a tribal, us vs them, hostile environment where one belittles their neighbor. We can have interesting, fun, insightful technical debates! There's no reason to devolve into bigotry to "win" an argument, it's a lot more fun to learn from each other and do cool, new things.

[1] https://www.win.tue.nl/~evink/education/avp/pdf/feel-of-java...

I don’t see it as winning and losing. I see it as deciding where to stand and where to contribute in my technical life. How do you personally trade off velocity (code fast) vs correctness, completeness or performance? Javascript generally values velocity over generalisability. (“It’s better to implement something quickly and worry about adapting it to other problems later”) compared to most Java code. It’s not necessarily better and worse. It’s a question of fittedness with your own values and the values of your project.

If you consistently write code which fights the ecosystem your code is written inside of, it’s quite painful. Writing Lua without ever blocking is hard. Writing javascript with large, deep class hierarchies is awkward. I’ve seen people write pure functional Java, but if that’s what you’re into you should probably consider just using a different language.

And to name it, Java does not feel playful and flexible to me. Not compared to ruby, Haskell, python and javascript.

> Java feels playful and flexible.

Insane that anyone could say this with a straight face

>Javascript values programming velocity, accessibility to new programmers and simplicity.

You are joking aren't you?

No, I'm not. Or at least, not as I see it.

How would you describe javascript's values, compared to other languages? This is a great chart / explanation of this kind of thinking from Bryan Cantrill:

https://www.youtube.com/watch?v=2wZ1pCpJUIM

No integers. What's the difference between == and ===. Why do I need to learn two ways to declare a class. What module system should I use and why are they all different. Why isn't there much of a standard library. WTF is Grunt, NPM, etc. Just explain 'this' to me again. What is the distinction between null and undefined. What is the difference between for/of and for/in.

JavaScript is far from accessible to new users neither is it simple.

You're arguing that javascript doesn't succeed at being accessible to new programmers. I'm not arguing that. I'm saying that javascript nominally values about being accessible to new programmers. There are way more novice programmers using javascript than (almost?) any other language. This isn't an accident.
I agree with most of your points. But the distinction between null and undefined is an important one. Its absence is a legitimate complaint about SQL.
In A Philosophy of Software Design the author talks at length about this and proposes that "deeper" modules (classes/methods/functions etc.) provide the most cost/benefit ratio, where the interface of a module is the cost and the functionality is the benefit.

Code doesn't magically become less complex by hacking it into pieces.

No hacking code to pieces makes it more complex. The trade off here is that the code becomes more modular.

Whether you want your code to be more modular is an opinionated decision but most people don't realize the benefits of high modularity. Almost all major design mistakes that necessitate code rewrites come from lack of modularity.

There are also a lot of cases where people "modularize" code without actually modularizing it --- they extract certain functions into separate modules or files just to break up the current file, but that new module they've created can't function or do anything on its own outside of the context it was extracted from. So in these cases they've really obfuscated the code in the name of "modularization", but the code is no more modular than it was before -- it's just more obfuscated.
Right; breaking it up doesn't necessarily make it more modular, it just necessarily spreads it around. This is -a bad thing-, with no other context. The hope is that it modularizes the code enough to enable better understanding/reuse/extension (thus being a necessary evil).
Right. The problem I've seen way too much is modularizing code too early in its lifecycle. This mistake seems to happen a lot by smart programmers who are inexperienced.

The instinct seems to be that they want hinges in their code, so their code can adapt and be reusable between projects. But they don't actually know where the hinges should go, because they don't need them yet. So they just put hinges all over the place - even where hinges aren't useful. If the metaphor is confusing, I'm talking about things like making an interface around a class, when there's only one implementor of that interface anyway. Or breaking a complex function into small, "reusable" pieces spread over multiple files - except where those small pieces are only ever used by that one call stack anyway. (And where they aren't that semantically self contained.) The result is harder to follow code with no actual benefit. And the resulting code is almost always bigger and more complex, and thus harder to work with.

Usually code thats the easiest to refactor is code thats the easiest to understand. That means, small, dense, correct code, with a simple test suite. If you write code knowing that you can (and will refactor) later anyway, the result is almost always better software. You will come up with a better design after you know more about your problem domain. Plan for that, and set yourself up to take advantage of that knowledge later.

> Right. The problem I've seen way too much is modularizing code too early in its lifecycle. This mistake seems to happen a lot by smart programmers who are inexperienced.

I don't see this as bad. Modularization protects against an uncertain future. Most code that's modularized is only used once and this Looks bad only because you haven't seen the alternative of what could have happened if code wasn't modularized.

Non modularized code is often designed wrong because of an uncertain future. Once the project is too far down the line, people just keep piling technical debt on top of the design flaw. Code is rarely rewritten until it's at a point where it's horrible than it takes a massive engineering effort to rewrite and even this rewrite could be wrong.

The alternative is code littered with modules that are used once. Which is better? Obviously the more modular code.

If you can move your code and spread it around it is Modular by definition. This is never a bad thing from a design perspective. It is only a bad thing from a complexity and readability perspective.

More likely you think it's a bad thing because your code isn't actually modular. Likely you need one piece of logic but that logic isn't modular so to move it to another location you need to drag a bunch of extra baggage around with it. You wanted a banana but instead you got the gorilla holding the banana and the entire jungle. Sound familiar?

The smallest primitive that is modular is a pure immutable function. If the modules you are moving around are not pure functions then likely your code isn't actually modular.

> If you can move your code and spread it around it is Modular by definition.

For it to be truly modular, you also need to be able to use it in multiple contexts. I could take any random 5 lines of a complex function and pull it out into another function in another file, but that doesn't guarantee that this was a smart thing to do in that particular scenario. What I'm saying is there are tons of times when people do this merely to get the linter to pass, instead of for the actual purpose of modularization.

I would say the file thing is unnecessary. That's just an OCD thing. You can break stuff up and keep it within the same file, it literally has the same effect.

I understand why you think that type of modularity is bad, but it is actually good. It only appears bad because most of the "modularized" code is only used once.

No one can predict the future so the way to minimize rewrites is to make composable units of code that are small and highly modular. It's not about breaking up your code. It's about writing small logical component then building up the larger component by composing the smaller components.

This results in a large number of modules that are only used one time, but it prepares your code for the inevitable point of the future where you find out the design was wrong and you have to rearrange the logic.

When such a time comes you most likely just trivially rearrange some of your logic and add additional pure functions into your pipe line for any actual new logic. The majority of your modules remain untouched and this only seems bad, but it prevented a rewrite of the entire framework.

The timeline where the definitively worse outcome occured didn't happen because your code was too modular. So you have no point of comparison and you assume the modular code that is hard to read is bad only because it's hard to read. You failed to see how it prevented a massive refactor.

Usually if code is so unmodular, people just live with it and keep accumulating massive tech debt on top of everything. If all your seeing is tiny functions everywhere then this is definitively better than the alternative.

From a design perspective highly modular code is always better. From readability perspective though, you are right, modular code is harder to read. But there are ways to mitigate this.

Hacking code into pieces does give one the opportunity to name the pieces, though

Mixed blessing, that

> name the pieces

Solved without splitting into functions.

https://ilya-sher.org/2019/10/21/section-syntax-next-generat...

I completely agree.

Another similar issue that I see a lot in both Java and C++ codebases is "premature wrapping" of foreign APIs. Basically when building a program that has to consume a certain API that is somewhat incompatible, every single concept of this API is wrapped in a separate class before any planning, to the point each one-line procedure call turns into a 20 line class.

Of course, after the wrapper is written, the program still needs higher lever abstractions that use those wrappers. But since zero planning went into the design, now you need exactly the same call order as before, however instead of an ugly (but simple) procedure call, you have a class wrapping it, and to understand a simple workflow you have to go trough at least two layers of classes.

Ugh same. Very common that I get slightly irritated by folks who blindly wrap something with no real reason other than "somebody else wrapped something similar so I'll wrap this for consistancy". Too many juniors thinking that they need more files/PR due to impostor syndrome...
Having worked in games, my pet peeve is the cottage-industry of amateur game engines and Youtube game-engine series that are pretty much just that: wrappers around OpenGL, SDL, Entt, Imgui and a multitude of other libraries.

Most of those never really produce a game, since the authors know how to wrap the libraries, but the engines don't have enough substance to help making a real game.

A notable exception however is Casey Muratori (of Handmade Hero), who actually skipped the wrapping and went for a more direct code. Interestingly he has a nice inversion of control architecture.

I think it's a combination of effort and culture that leads to such differences in style; in C, creating objects and functions and overriding them etc. takes far more effort than it does in Java (where IDEs can also generate tons of code automatically), so programmers naturally think more about whether the additional effort expended is worth it. Asm is an even more extreme case in the C direction --- every additional machine instruction is explicitly written, and so is dispensed with if not absolutely necessary.

That said, I've also seen "object-oriented obfuscation" in C, so some people seem to just love complexity and writing tons of code to do a simple task, or were taught "abstraction is great, use as much of it as you can" and never thought about when to stop.

Overabstraction usually increases macro-complexity while decreasing micro-complexity; a function with a single line of code is "simpler" in that its immediate purpose may become obvious, but having to mentally stack from its callers means that the big picture is harder to comprehend.

At the extreme high end of density are languages like the APL family, where the density is so high that the "big picture" becomes a slightly smaller one, and Arthur Whitney has been famously quoted as hating scrolling; but at that density, you can no longer "skim" large portions of code --- instead, each individual character needs to be read and pondered carefully, because each one says a lot.

>>In Java it would be a nest of SortComparator interfaces and SortAlgorithm implementors, which would act to hide the algorithm itself.

Java developer here, no such thing. I guess it varies based on who is doing the coding. Although, I did start out as a C developer.

I would only write code like that if the use-case called for it. Otherwise, no.

Writing code is more of an art form, some times you may need to do crazy stuff like that, but a lot of times not.

KISS

You’re essentially arguing about Abstraction which has its pros and cons. Leaning on one of those sides while ignoring the other is a trap for inexperienced players.