213 comments

[ 2.2 ms ] story [ 218 ms ] thread
> Unfortunately Java’s structure seduces the unwary programmer into excessive abstraction. Between demanding a class for everything and enforcing all types at compile time, it encourages you to lay down layer after layer of formalism in class and interface declarations.

To me, it's not really about "seduction", it's about inviting the programmer to abstract away with tools that are heavily unfit for the job. If your only means of abstraction is a class, everything starts to look like a method call.

That's why I enjoy Common Lisp's ability to create macros on multiple stages of the language's operation: reading code, compiling code, substituting function calls. These make it possible to introduce new syntax where necessary, which in turn makes the code shorter, and to write compile-time optimizations that do not get in the way of the main function bodies.

The abstraction is abstraction. It does not matter how it is implemented. Might as well do 100 levels of function pointers or simple nested calls with no classes at all.

I think the author's main idea is that your methods should carry some weight and be long enough so you can visually trace the logic. Do not write functions that do nothing but multiply 2 numbers.

I can agree with the second paragraph but I disagree with:

> The abstraction is abstraction. It does not matter how it is implemented.

To me, it does matter. A good abstraction should not get in the programmer's way. This directly depends on making the language's syntax support the abstraction in question. This, in turn, depends on the macro system of the language or lack thereof. Finally, that is a question of the given language's expressiveness, or lack thereof.

You can have all the expressiveness in the world and still write those 3 liners. Or you can have plain functions and still write decent code with abstractions where needed. Basically good master will try to make decent job with any instrument and bad one will mess up everything disregarding the tools given.

Of course on its own more expressive languages make job easier to write and read. Not denying that part.

Yes. That is the part that I agree with.
You don't need macros, you just need a language that isn't awful. Plenty of languages make writing a plain datastructure and values of that datastructure easy, make "good code" just as concise as - if not more concise than - "bad code".
Abstraction is good when it lets you not thing about something and still understand how the system works.

Abstraction cost is that the system becomes more complex overall.

Good abstraction makes the system easier to reason about and leaks rarely, so you rarely have to think about the insides. But all abstractions leak under some circumstances.

If you have 100 layers of abstractions at any given moment some layer leaks and you're constantly fighting the system without being able to understand how the whole thing should work.

Language syntax only helps by removing the need for some abstractions by including them in the language.

> Do not write functions that do nothing but multiply 2 numbers.

Maybe I'm missing something, but doesn't Java already have a function that multiplies 2 numbers, as well as a fancy infix notation for calling said function?

The problem is one that I've witnessed, but uh:

> Good Java should resemble C

The recommendations are a mixture of things that are... questionable, and things that really apply to basically any big refactor regardless of paradigm, language, or problem.

But getting back to the issue at hand: what I see from the Java/C# world is a mentality of "abstract unless there's a reason not to", instead of "don't abstract unless there's a reason to".

Charitably, this approach might be compared to the micro services pattern: if you're shipping your org chart, and your org chart is thousands of people across four continents, then maybe eagerly building out those fire-walls of decoupling can be useful. Stereotypically, at least, this seems to line up with the kinds of companies we tend to think of as using these languages.

But where it becomes a big problem is when it's used in a one, two, five-person project, and all those walls just become massive speedbumps, not only slowing you down but making it hard to drive straight.

> Good Java should resemble C

If that sentence from the article is the only thing you took issue with, I'd wager you interpret it different than the author's intent. I took it as low on abstractions and mostly imperative.

It wasn't the only sentence I took issue with, it was just the one that raised my eyebrows the highest.

> low on abstractions

I feel with this that the author is going to the opposite extreme. The takeaway isn't that abstractions should be eliminated categorically, just that they have a cost and shouldn't be fetishized.

> mostly imperative

I take issue with this too, though this part may be more a matter of personal preference. Yes, it's worse when your imperative logic is split across 15 files, but I see imperative logic itself as something to be done sparingly. Imperative logic means mutation of state, means easy-to-lose-track-of side-effects, means unpredictable behavior. This is sometimes unavoidable at the systems level (where C is typically used), but it is absolutely avoidable at the application layer (where Java is typically used).

Here's another one that raised a red flag:

> A well-considered class...should be easy to name, ideally with a noun that corresponds to something in the real world with a lifecycle of its own.

Real-world-modeling was found a long time ago to be a bad approach to OOP. The author is right to say that classes should represent clearly distinct concepts that are easy to name, but there's no reason those concepts also need to map to real-world entities. Often, they shouldn't.

>means unpredictable behavior

There is something to be said about making sure it's clear where and when state mutates. But functional code has it's own set of pitfalls. I've seen otherwise good devs struggle like crazy reasoning about complex functional code. (Especially when it comes to performance)

> Real-world-modeling was found a long time ago to be a bad approach to OOP. The author is right to say that classes should represent clearly distinct concepts that are easy to name, but there's no reason those concepts also need to map to real-world entities. Often, they shouldn't.

I 100% disagree with this. Most applications are modeling domain logic and business processes as code. And ideally as many of your classes as possible refer to a concept or should translate to simple and understandable objects in your domain. In an ideal world every class would be named after a concept/entity your domain expert understands and uses.

*Unless you read real world objects as physical objects, but I think the author meant real world as in domain concepts.

Yeah, from the title, I initially agreed with him, but although he's definitely right that over-abstraction leads to some horribly painful Java code, he seems to be arguing in favor of the other extreme: an abomination that I've been seeing more and more of in the past five or six years where nothing is a class unless it absolutely has to be (i.e. when unmarshalling XML or JSON or reading data from an ORM). In Java, this means that everything is declared static, including data. And in a horrible, unholy way, it does resemble C, right down to the non-thread-safety.
> But getting back to the issue at hand: what I see from the Java/C# world is a mentality of "abstract unless there's a reason not to", instead of "don't abstract unless there's a reason to".

When I was starting out and reading lots of books and blogs this was very much my mindset.

Must use all the patterns!!!

But after working on a huge code base with many many many layers of abstraction, I now am of the mindset of just keeping it dead simple. If I need to get something from the database. Just get it. Introduce an abstraction when it’s needed. It’s easy to add an abstraction, it’s harder to take it away.

Cargo cult. One interface per one concrete class. It drove me nuts, it still drives me nuts that we are still having debates about it in 2020.
What's more annoying is when they import this into other languages that aren't hobbled by the design features of Java.
"modern" PHP frameworks suffer from this idiocy to no end.

I'm sold on "pure PHP" but you can get a hell of a lot done with a few simple libraries and avoiding all the bullshit pushing you into classes.

Typescript is quickly falling down this rabbit hole as well.

Which Typescript projects have you seen been over architected? In my experience most typescript tends to be sane. Interfaces are only used to describe shapes of objects, class interfaces are a rare and only used when the consumer needs that abstraction.
It varies widely by org. TypeScript is intentionally designed to accommodate as wide a swath of styles as possible (even to a point where some functionality is duplicated: you can have enums or string unions, you can have interfaces or object types, etc). TypeScript code can be written squarely in a C# developer's comfort zone (unsurprising, given it's developed by Microsoft), or it can be a richly expressive and flexible immutable/functional/structural type system. It's similar to what I've heard Scala described as (though I haven't used that myself). In some cases though this is a strength: I usually to stick to a functional style where I can, but end up with a handful of classes where they make sense. Being able to mix the two smoothly is nice.
TypeScript and modern JS are doing more than ever to get away from classes. Not sure what libraries you're using but it's time to reevaluate.
It drives me up the wall. If you have interface X and the only implementation class XImpl, just put the damn code inside X. If you ever need to have two implementations, make X an interface then.
To be fair, if you unit test your objects, there are usually two implementations - the real thing and a mock. If you have a solid mocking framework, that can alleviate this need.
I have moved into thinking that mocks don't really have much value. Testing real things is the only way to verify real things.

If you can, set up a real DB, cache, whatever, and run your services in tests with real instances of their dependencies. Obviously this falls through for external APIs and things that are very expensive, but your savings in "But the tests passed" confusion time will be huge.

I've come to this conclusion. Organizations will spend many years writing and maintaining dubious unit tests that break with the smallest change, when they'd get an order of magnitude more value spending that time on an automation suite. One of the biggest reasons is testing at the same unit of granularity as the spec means you're tests break when the spec changes, and usually unit tests are far too granular.

*I still think unit tests are great for tricky logic, but a lot of coding is just gluing together different systems where unit tests don't have a lot of bang for their buck.

I might-should've rephrased my original post to suggest that mocks for non-code-you-wrote deps are a waste, but being able to inject different implementations of code you wrote for the sake of testing still has value.

If you want to test how your code behaves when getting/setting data from a database, you fundamentally cannot mock the database. If you want to test how your code forms arguments to send to a dependency, you probably can, as long as you do both. If you're just testing that your args are correct without actually using them, you'll never really know.

A specific example stands out in my mind when I watched a TDD-minded developer suggest that their code was complete because they injected a mocked version of DynamoDB into a service and then verified their tests ran. They committed and pushed without actually testing against DynamoDB. When tasked with _actually running it_, they found that there was a fundamental flaw in the way that developer perceived Dynamo's behavior, which obviously meant their mock was totally useless.

If you want to test that you're pulling the right ID off an object to send to a database you can mock. If you want to test how your code actually behaves with a database, including failure scenarios and invalid arguments, you cannot.

As usual, solutions are much more nuanced than I previously stated. I've moved away from mocks, not banished them entirely.

Yes, there's a pretty fundamental problem: does your mock match the behavior of the real component? You can never be sure, unless you write extensive tests that compare your mock to the real component. And then those tests need the real component.

But mocks could be useful for testing that your code correctly handles errors that are returned by an external component.

I’ve seen this over and over. The engineer building the mock is the same that wrote the code, so the same incorrect assumptions get baked into both.

This is what makes mocks of limited utility. In the end, they don’t prove the code works and they are expensive to create and maintain.

Yep. I kinda also think that the dev that wrote the code shouldn't be the dev that writes any of the tests, but that's much more of a philosophically sound but in-practice poor idea that I think about sometimes. I think it has an array of benefits that go totally unrealized if a person writes both. Things like, if your code is too complex for another engineer to write tests for, it's too complex for your team to maintain. If code standards aren't consistent enough in your codebase that other engineers feel the need to refactor/redo blocks of code in the name of style/readability/whatever, your codebase's style isn't automated well enough. Things like that. I just think it's so much of a better strategy, unfortunately up front it seems like a big time sink for (at surface level) little game.

I also refuse to measure test coverage in my codebases. "How frequently do bugs show up in production" and "how frequently are bugs fixed without adding tests" are metrics I find valuable but are underrepresented in the testing space. If bugs don't make it to prod very often, and when they do they are fixed, your testing strategy is probably sufficient. There's no reason to write thousands of null checks and formatting validators if you don't need to. Tests require as much or more maintenance as code. There's no reason to write more of them than you need.

Some things can be mocked quite successfuly -- like ORM stuff. You can also write an integration test for the mocked component, let the rest of the code blaze away with the mocked version, it is usually a very good compromise.
One of the big reasons for this that I've seen is that it's easier to measure "coverage" with unit tests. Many managers and engineers like things that are easier to measure, even if they're measuring the wrong thing. It can also be a cover-your-ass strategy--if a major catastrophe occurs, you don't want to be defending your decisions without data. From that perspective, "we feel like we had sufficient integration test coverage" is much worse than "we have 95% unit test coverage". You don't want to make it easy to become a scapegoat, and mountains of data are good protection, whether they're relevant data or not.

I too generally only write unit tests for tricky pieces of code, especially when it's dealing with concurrency which tends to be difficult to test deterministically at the API level. There are other good reasons to write unit tests, such as execution speed, complexity of setting up a testing environment, faster development feedback loops, etc.

It's actually not that hard in most non-embedded software testing to get code coverage for any test stage. Even up to manual testing. The tooling exists for all major languages. I recently wrote an article on the subject which links to tutorials for Java and C#: https://docs.teamscale.com/howto/recording-coverage-for-manu...

Maybe that'll help you convince your managers to invest in the kinds of tests that make sense for your product.

> set up a real DB, cache, whatever

You should shoot for both, actually. If your tests require a live DB to run, they can't be automated (or they can be automated, but failures won't tell you much useful).

There's a non-unit-test value of having good code coverage, too: being able to run any function independent of the rest of the app, with arbitrary inputs. When you have a problem that's difficult to reproduce in a controlled environment, it's really useful to be able to run a function that's normally ten function calls deep all by itself over and over again while trying to isolate a problem. If you have to run the entire app to run any of it, that becomes impossible.

> You should shoot for both, actually. If your tests require a live DB to run, they can't be automated (or they can be automated, but failures won't tell you much useful).

Can you define a scenario in which hitting a real DB wouldn't present useful information while a mocked DB would?

Sure, if the DB is down. Or if somebody else deleted the data that your test relied on. It'll happen often enough that you'll stop paying attention to failing "unit" tests.
> Or if somebody else deleted the data that your test relied on

If you're relying on a shared database you're already not really testing effectively. I don't really think this is a scenario to worry about (meaning, if you're in this scenario, you have bigger things to worry about).

> if the DB is down.

An interesting suggestion.

When you have multiple teams pushing changes and a limited mumber or DB licenses and you want your CI pipeline to run faster, mocks are a very good compromise. DB bugs rarely happen if you don't change any DB stuff after a couple of years in a project, at that point you are really confident in your DAOs and ORM services and you can mock them. You write integration tests for the DAOs and ORM, but you rely on the mocked version in 95% of the test cases.
If your tests make you write worse code, what have you gained?
I finally have words to describe why I prefer behavior testing over strict definitions of unit tests.

The use case of mocks/stubs/etc for me is “this thing is expensive to execute or external and can’t be validated directly”. In the former case, validating the inputs to the expensive interface is buttressed by thorough and verifiable testing of the thing it calls into. In the latter case, you provide an interface that behaves according to your understanding of the external service without requiring its availability.

In pure unit testing, the “external” boundary is hungry and consumes as much interaction between code components as possible. In behavior testing, you extend end user validation as far as is practical or possible.

The benefit of that is you can verifiably satisfy user needs up to the limits of your testing/testing environment, in user language. Wherever you need to silo who the “user” is (eg service/api boundaries), you can do so. But your tests are designed to validate user assumptions first, and fussy technical details after.

So if the test is red, it verifies that the users assumptions were wrong first, right? ;)
Sometimes though you can expect to have another implementation at some point in the time however. A good design anticipates which are the key interfaces where this applies. Second thing is that sometimes interfaces can act as binary firewalls. Recently I was able to split a few classes conveniently into a shared library since their functionality was expressed in a pure c++ interface.
Having a clear, short and concise overview of public API is a good thing. interface allows for that. Object Pascal had this feature by default: you have to declare public methods and then implement it in a different section. I always missed that in Java and separate interface allows to recreate that pattern.

Sure, you might want to say that I should just use IDE which will show me all public methods in a GUI tree control. But I don't like it, I like code. May be some feature which completely hides anything in the class except public method declarations could substitute, but I did not see that feature yet.

Honestly I don't know what's wrong with an interface, even if there's a single implementation. Idea and Eclipse allows you to jump to implementation with a single click if that's what you're looking for. JIT will replace virtual method calls with direct ones.

>Honestly I don't know what's wrong with an interface, even if there's a single implementation.

I once had to work on a code base where every SQL query was inside a DaoImpl (ok) which was an implementation of a Dao interface (questionable) which is then called by a BoImpl (why on earth?) which is an implementation of a Bo interface. By your logic it would be sensible to add even more interfaces even though the intermediate classes do nothing except pass a function call to the next layer. Want to add one parameter to an SQL query? Well vbezehnar said it's fine to modify 6 or 8 files to accomplish such a trivial task.

The irony is that the whole code base was garbage and hardly readable and the biggest culprits weren't the overuse of patterns (they were just the cherry on top) but rather the fact that a random bundle of libraries has been misused for purposes that they were not designed for. My personal favorite is keeping hibernate objects beyond the lifetime of a hibernate session and the hibernate session was configured to live as long as the http request. Combine this with a web framework that completely abstracts away http requests and it means you can never use hibernate correctly because you never know when the session has expired.

I don't know why leaving a class package protected is such an eyebrow raising thing. Even code analysis tools flag it with a warning. Most classes shoud not be accessible outside their package. That would raise some questions with guys who advocate lots of interfaces -- where and who exactly is going to need this interface?
Honestly I think most of the OOP world simply doesn’t “get” interfaces/protocols/contracts/etc. Sure, there’s polymorphism (but it’s typically inheritance-based) and inversion of control (but it’s typically used for testing). But like you said, cargo cult.

It’s hard (okay it’s not hard, it takes study and practice) to know what the “right” abstractions are, and it doesn’t produce IDE solutions (unless you’re working from a strong theoretical foundation).

It certainly doesn’t help when the type system doesn’t help. Lots of great abstractions are available when you can derive and infer meaningful types and derivatives at the end interface without a lot of fuss. It doesn’t just improve DX for library and product devs, it also improves design instincts.

A more functional style often fares better in my experience. Functions just accept “some data” and return “some other data”. Even without Haskelly algebraic data types, “some data” in a function that isn’t explicitly tied to a concrete type is a good prompt to think about the kinds of things that are common or disparate about the data and how to handle them.

It doesn’t necessarily lead to fantastic patterns. Most functional-style-but-idiomatic TS is not as well designed as equivalent code in an ML, error handling can be hell. But at least in my experience it has better outcomes than the OOP equivalent.

I keep hearing functional, functional, but all(not that much) the functional code I've seen is no better than your average @Service calling anothrt @Service calling anothrt @Service. Yeah, it looks a little better with less parentheses, yeah you get fatter instead of taller code which is not that much easier to understand.

There's still a milion ways to do the same thing, and there are still smartass people going out of their way to produce code that makes people notice how unique they are.

All at the expense of wondering what the memory and performance will be.

Yeah, that seems like an overkill to me.

The balance I do like to strike is to use interfaces for any classes that interact with the outside world, and then have those faked out.

That way, I can use them to provide data to smaller units, or record that business logic recorded the data I expected.

Obviously this doesn't fix if the DB interactions themselves are correct, but it's a start.

> One interface per one concrete class

I vaguely remember language support for something like this in the SatherK dialect of Sather. Sather itself was inspired by Eiffel. One-interface-per-class actually is no problem if it's well supported in the language, by simply adding a single special character to the concrete class name.

It worked something like putting some sigil (dollar sign, IIRC) in front of a concrete class name meant that the code was referring to the public interface exposed by the class. (I could have gotten it backward in that maybe you needed the sigil in function signatures/field declarations if you wanted to force the concrete implementation instead of the interface. I think it evolved from the normal Sather dialect, where interface declarations looked pretty much like class declarations except that the name began with a sigil. If it hadn't evolved from standard Sather, I suspect they would have required the sigil if you really meant to intentionally restrict your code to the concrete implementation.)

Java could have headed off a lot of premature abstraction by making it easier to later replace a concrete implementation with an interface. Instead of making "new" an operator, Java should have made "new" a static method returning an instance of the class, with a bit of compiler flow analysis to allocate an object the first time you referenced "this" within "new". The chief advantage is that it would have minimized the amount of code change necessary to replace a concrete class with an interface if needed. If new is a static method, then the interface's "new" can return some reasonable default implementation, or decide among implementations at runtime.

You'd still need to recompile everything, so that all callers would use the invokeinterface bytecode instead of invokevirtual. Alternatively, invokeinterface/invokevirtual could be collapsed into one opcode, and rely on polymorphic inline caches/hot spot inlining to remove most of the method dispatch overhead. A third option would be to have the bytecode verifier perform invokevirtual/invokeinterface substitution at class load time in cases where a concrete class has been changed to an interface or vice-versa.

Making "new" static factory methods instead of an operator would also make it possible for a class to transparently swap in specialized subclass implementations (for instance, specialization for BigInteregrs that fit in 64 bits, or Strings where all codepoints are less than 256). PyPy does a small amount of this specialization internally for Python, but it would be nice to have language-level support.

I think this issue stems from the lack of focus on components. To me, if a class is inside the component and has no external visibility, there's no need for it to have an interface. On the other hand, if you're exposing functionality to something "outside" the component at hand, you should definitely be using an interface as you cannot predict the future.
I have followed an academic path so far, so none to little formal training and lots of room to try out my own ideas. Often ten lines of Python can already be a significant improvement over using some convoluted 'easy-to-use' GUI tool (looking at you, Origin...).

I started exactly like this, and the pattern I wanted to use/learn was 'classes'. After struggling for a while, I realized that what I wanted to do was much easier to solve with a 'table' (list of lists) and creating functions that return the resulting table modified. It taught me that there is value in simplicity, in particular at the level you understand right now, or are at least not too far outside of your knowledge-zone. If you want to get things done, that is ;) Note that this was a few years before scientific Python and its ecosystem came into full-swing, I'd probably have discovered and uses Pandas right now.

Later I learned that this was called functional programming, I learned how classes work and what they are useful for (far larger programs than I was writing at the start of my career). C++ is a fixture in the scientific community (I'm now into Monte Carlo and solvers), and while I'm happy C++11 happened, the functionality of the C++ stdlib enables so many different styles that it is really hard to get a team to stick to a subset, and not get too distracted with all the nice new tools. Sure, much of it is nice, but how we can write C++20 is pretty much another language from C++03.

I guess I rambled a bit there. What I'm saying is that I'm generally critical of using new features, unless I've seen a convincing and for me personally concrete demonstration of how it saves mental CPU-cycles or boiler plate. This makes it easy for me (in the end, I'm not here for the code but for what you can do with it) and onboarding others (who, in science, might barely know that C++ has versions).

KISS KISS KISS. Nothing will convince me of the opposite :)

> Sure, much of it is nice, but how we can write C++20 is pretty much another language from C++03.

why is that a problem ? in the end ideally each of your program should be its own mini-DSL that solves the problem at hand in the best way ; successive C++ versions just allow your to create more and more expressive DSLs

If every program is written in its own language, we're going to have a hard time collaborating on code!
We had a vendor deliver some C# source code that was exactly this. A 'repository' interface that had a concrete class that called an abstract data access class etc. A path of 3 or 4 calls to run a SQL statement against a MSSQL Server. There were no requirement for this.

The Gang Of Four design patterns are great when you actually use them to solve the problem they were designed for. I worked on a product that grew in complexity and a factory was needed. Implementing was easy in Netbeans - right click and extract interface and provide the new implementations and the creation logic.

I'm not sure when using patterns all the time got so prevalent. As the saying goes, when all you have is a hammer, everything looks like a nail.

That's pretty typical in C# land (another is service->repo->data access with DI/interfaces for each). Very likely the dev had that pattern down as a reflex. There was no consideration on their part because they already knew a solution that worked. They could move on to the other problems or requirements.

As another poster elsewhere said, the annoying thing is only the single implementation for the interfaces, and no real code that makes use of the abstraction. It only really alleviates other devs from asking "where are the interfaces for these classes?"

Using an interface forces one to think about a clean API, that does not rely on internals.
In theory. Sometimes it only has one implementation and the details leak through.
That doesn't make any sense. Java already has private and public as modifiers. The only meaningful difference is that interfaces prevent you from using private. That's such a shallow benefit. If you have the discipline to create interfaces you also have the discipline to use the right modifiers. The reality is that all those interfaces will just mirror the class and cause extra friction even though polymorphism is never needed.
(comment deleted)
> Must use all the patterns!!!

In my experience, Java developers don't use all the patterns, they use exactly two: Factory and Singleton.

That's the beginners. Average ones use plenty of Adapters and then build Facades. The cool ones use Strategies. They all suck.
Yes, with experience you realize our job shouldn't be about knowing as many different techniques as possible but rather knowing how to manage complexity by using those various techniques.
I run a small codebase,where 99% of code is done and dusted and very unlikely to be revisited for years to come. Most of it is written as one off solutions. Could I have written with all the abstractions? Would it have helped?No. So here I am, sitting and thinking whether I'm stupid not to or whether I simply saved the business from using countless hours of unnecessary abstraction.
2 weeks ago was my 11th birthday as the sole developper in my workplace.

I embraced KISS maybe 2 or 3 years after starting at this company. I NEVER regretted it.

I still maintain a few projects started in my non KISS period. I regret every time I have to touch the code base.

Of course YMMV.

My version of this philosophy is, "Write code a junior developer can maintain." That means unless absolutely necessary don't use unusual language features, use the most common framework on the market for that problem set, no unnecessary abstractions, etc. So far it seems to work well.
Programming languages are like any other language. If you use lots of complicated words every time, nobody will understand you.
It goes both ways.

I've seen beautiful abstractions that make a lot of sense, a la DDD that saved companies a lot of money. I've seen a senior architect almost shed a tear when I told him I loved the way he used the Bridge Design pattern in a project and how easy it was to implement some feature because of it (I don't remember the example.)

> A well-considered class should own an interesting decision and the data needed to make it, not just one or the other.

This is not about abstract/non-abstract, Java, C or C# .

This is about 99% of people not being able to write decent OOP code. This is not about "programmers" not getting that first awe inspiring feeling when they first see decent OOP code, it is about not caring, not event after 15+ years of programming OOP languages.

This is why I always see endless @Service and @Entity code, completely separated, like it's 1996 and we have procedures and structures in Borland Pascal.

Apparently it gets missed a lot that Java and C# build on top of C and C++ enterprise culture, which was bubbling with GUI frameworks, CORBA and COM/DOM when Java was designed, and later .NET followed on (given its origin).

It feels so strange to make C++ a kind of better language, when it was one of the main creators of that abstraction culture.

Naturally, in the last 20 years, the only C++ developers are those that didn't moved on, so now 90's C++ code gets coined with "writing Java in C++", the irony.

> abstract unless there's a reason not to

Is the _exact_ behavior I have seen in 100% of the Java and C# codebases I've used.

> But where it becomes a big problem is when it's used in a one, two, five-person project, and all those walls just become massive speedbumps, not only slowing you down but making it hard to drive straight.

I think that most patterns exist to prevent bad things from happening, or to make things easier to understand. I think that in a team of 1-5, there just isn't enough output happening to make either of those things huge concerns. If you're very small, there's not a ton of reason to try to protect yourself from future-scaries that don't exist in your company yet.

And then, if the response is "But what about when we grow", that's literally the exact right time to abstract behavior. _As needed_.

On the “Good Java should resemble C” front, I’ve jumped onto plenty of projects that had methods taking char and I guarantee you those methods were not easy to understand. And the debate on using typedefs to add type checking for things that are just an int underneath is alive and well just as wrapping an int in an object is in Java.
The problem is not the language. It is possible to write very elegant and well-structured software in Java. The abstraction bloat usually seen in Java software comes from somewhere else, and I’m not sure where. Is it a symptom of corporate software? Is it Java’s popularity that attracts lots of mediocre programmers? Or is it that Java is often used for solving complex problems that are by definition hard to get right?
I tend to believe in environments like Java or C#

there's shiittton of focus on architecture, patterns, interfaces and generally testability

there's shitton of discussions about those topics in those langs

So it may be reason why people tend to over abstract stuff.

In my experience languages have a culture of their own.

For example, C++ has a culture of caring about low-level details to squeeze out maximum performance. I once found myself writing something that wasn't performance critical in C++ due to a library I needed, and just minutes later I caught myself reading up on move semantics for maximum efficiency. I couldn't help myself getting sucked in by the language culture!

Java's culture, in my opinion, is one of abstractions. There's little in the language to force you down that path, other than decades of it being the prevalent way Java is written, which bleeds into the standard library, core frameworks, and becomes the Java Way of Thinking.

Totally! Java developer's over reliance on design patterns and abstractions tends to enable atrocious designs, very often the first idea that came to mind at a very early time in the design process, clambering on to a monstrous implementation that somehow runs. In Python, the relative flexibility and pseudo-code nature of the language means that more designs get tried, and simplicity wins. In C++, developer self-selected into being hard-core, so you get hard-core code...
The unofficial motto of Java seems to be "whoever dies with the biggest stack trace wins".
Ironically the GOF book used C++ and Smalltalk, while Java did not exist.
Java's culture also seems to emphasize social class distinctions that mirror the corporations it is used in.

There's an architect programmer and a worker bee. Everyone wants to be the architect and to create the "enterprise" framework used by the worker bees.

> Java's culture, in my opinion, is one of abstractions. There's little in the language to force you down that path, other than decades of it being the prevalent way Java is written, which bleeds into the standard library, core frameworks, and becomes the Java Way of Thinking.

Yes exactly, that's why I personally stay away from Java. The language is totally fine, the tooling is also fine, performance is great, there's everything there in theory to build and maintain great projects, however, the culture is atrocious and overengineering is everywhere. And that's very hard to go against decades of culture momentum going this way.

> Java's culture, in my opinion, is one of abstractions.

Haskell's culture is also one of abstractions, but of a very different kind of abstractions than Java.

Java is used for a lot of integration problems, of course it has a thing for abstractions and interfaces.
Yet C# and PHP also suffer from the same problem so it's more than a Java problem. Verbose OO languages seem to give rise to cultures of over-abstraction.
It's partly in the language itself with everything needing to be an object and the way you have to fight the language to use lower level abstractions like static methods, out of the box the language is encouraging abstractions whether they're needed or not.

That's mostly just unpleasant though, beyond that it seems to be something deeply ingrained in the culture, even something as simple as a structure (a simple class with public fields) is a heresy in the java world, you need to make those members private and add getters/setters or you're likely to get warnings from the IDE and probably not pass code review. The enterprise world builds more and more abstractions on top of this but the problem is very deeply rooted.

A good part of the problem though is just programmers trying to solve hard meta problems instead of boring repetitive business problems.

Nah, the language drives its culture. When every IDE offers to generate 8-12 lines to "encapsulate" a single field and every coding standard tells you that the resulting code is better, it becomes very hard to escape the mentality that writing zillions of extra lines is how you do "decoupling" and what you should be doing.
Its not theat IDEs force you to generate code, its that IDEs are quick to hide code. I almost never read java files from start to finish, I just quickly navigate by methods/call hierarchy, in some side view, so its really easy for forgive Java for being verbose.

Actually I rarely remember java class names or methods, I just know a heuristic shortcut on how to find them.

After 15+ years of Java, I don't even see the boilerplate code, my brain ignores it like it ignores adds on webpages.

Imagine how much more effective you'd be if you'd been working in a non-boilerplatey language and spent 15+ years learning to see the essence of what code was doing, rather than having to expend that mental capacity on skipping over boilerplate.
When you work with huge codebases, 80% of the time is spent reading/navigating code. The IDE hides the boilerplate, I really don't mind it.
> When you work with huge codebases, 80% of the time is spent reading/navigating code.

And you don't see this as an indication that reducing code size is important?

Not really -- the IDE overview means it doesn't really matter if the codebase is 1X or 2X or 3X the amount of lines.

Then, I just think, excluding some horrendous language and some corner cases, experienced programmers are limited by their brain and not the programming language, just like certain languages that are more verbose are spoken faster and terser languages are spoken slower, but the information flow is done at the same speed.

I haven't had to autogenerate any code since adopting Lombok. It really brought sanity to the codebases I work on.
I used to use Lombok, but by the time you've got it properly working in all your linters, profilers and so on, it's just as much effort as using another JVM language. So I switched to Scala instead - much bigger reward for the same effort.
I don't know the reason but I can see this happening in PHP. Things that were very simple and straightforward are now obfuscated withing layers upon layers of abstraction.
> I don't know the reason but I can see this happening in PHP. Things that were very simple and straightforward are now obfuscated withing layers upon layers of abstraction.

Abstractions that do not fix any of PHP's problems by the way (like its chaotic std lib, its weird mix of dynamism and rigidity, bizarre alt syntax for control flow, all the error reporting cruft, adhoc variable creation as side effect of certain function calls, context-less output buffers, antiquated scoping rules...), PHP is just adding features to the language without removing the bad parts. The irony, it's so bad as being a templating language PHP devs are using another templating language on top of it(smarty,...).

Sun and now Oracle's business was to lend out consultants, so they benefit from a complex enterprise architecture.
Unit testing, leading to dependency injection, leading to IoC containers, leading to lots of tools to assemble, intercept and rewrite code at execution time.

But mostly unit testing. The commitment to isolated testable units of code is what generates abstractions of dubious value.

Don't use a class where you could use an interface: easier to mock. Don't use a static method where you could use an instance method on a collaborator: easier to inject, replace and mock. Your object graph getting too hard to compose, or other design smells? Don't worry, we have tools to handle the complexity, so you don't have to try and reduce it.

> Unit testing, leading to dependency injection, leading to IoC containers, leading to lots of tools to assemble, intercept and rewrite code at execution time.

it's funny since in theory TDD should reduce API surface as it is supposed to make people think about API before implementation. But then , TDD and DDD intersect and it's not much TDD anymore when the domain model has to be implemented...

Don't mock. Test production code, maybe with faked storage.
Often enough software has dependencies on external services (think of stuff like a CRM database, payment and shipping providers, integrations with CDNs, external identity verification services) where one has to go with mocks for testing if stuff like error handling etc. works.
Fake external storage / rpc dependencies.
That's called mocking...
Mock: Adhoc guessing of what methods called on a dependency, but sometimes even between classes in the same module, might return. Guess repeated over and over as new tests are added, sometimes tens of times or even more. For example, https://site.mockito.org/#how, "when(mockedList.get(0)).thenReturn("first"); System.out.println(mockedList.get(0)); // prints "first""

Fake: A replacement module that behaves like a production module, but with certain simplifications, for example in-process vs. using rpcs, or simply cleaning up the filesystem after usage. For example, https://github.com/tk0miya/testing.postgresql. "automatically setups a postgresql instance in a temporary directory, and destroys it after testing".

So... mock the storage? This very quickly becomes a slippery slope.
Fake, don't mock. Write, or ask the team that provide the external dependency to write, a small piece of code that behaves like your external dependency, but in-process. You'll thank me after about the 47th time you're guessing (inconsistently, possibly incorrectly, and definitely overly verbose) how the external dependency actually works. Ban mocking libraries.
Isn't that effectively the same thing? The fake implementation needs to mimic the API your software expects to run against, and must give valid-seeming results and also keep up with development of that external program for the variety of tests you'll run against it (which will grow, increasing the complexity of the fake). You'll of course have to do this yourself, because your vendor isn't going to do it for you. So now you have two problems.
Fakes are not mocks. The fake is just another module. Assuming no updates, it is written once. Mocks are written 47 times, inconsistently, while focusing primarily on other tasks. If there are updates needed, better to fix them in one place than chasing 47 test code locations with inconsistent usages.

Furthermore, these are external dependencies that can't be run in-process. If a dependency can be run in-process (aka library), there is no justification to ever mock it. I've even seen codebases that mock their own class B in order to test class A. Run the production code already. Ban mocking libraries.

Testing against external dependencies is only one form of testing.

Unit testing with mocks is another form. One does not replace the other.

Agreed. Using mocks while testing vanilla in-process code is never justified.
You don't need me to get on that bandwagon. I almost despise mocks.

Mocks usually over-specify implementations by setting up expectations of a specific implementation conversation rather than an outcome. They're painful to debug after refactoring implementation - they end up write-only - and they generally inhibit refactoring of the mocked API - often mocked instances of APIs outnumber production invocations.

I try to encourage people to replace control flow with data flow where possible; messages, command objects rather than method calls; iterators, streams and consumers composed together, rather than loops. Data flow can normally be trivially redirected into a container, and if the data objects are simple inert immutable tuples, they're trivial to construct as inputs or assert against on outputs.

Fakes are good too, better than mocks in most situations, since they are easier to refactor.

Although, IME, integration tests, while slow and often brittle (especially if you have any async components and define failure conditions in terms of timeouts), have a significant upside in permitting large factoring while still being able to test a substantial amount of end result functionality (i.e. the stuff that matters, not implementation details).

Java was full of AbstractSingletonBeanFactory classes long before unit testing became a thing, or at least before it was a widespread. I think IOC containers actually helped eliminate many of these abstraction overall, you need many less singletons and factories.

> intercept and rewrite code at execution time

I think "execution time" is one of the big issues, a lot of these patterns evolved from application server middleware like glassfish that would introspect and run packages and often provide services at runtime, sun and IBM were busy selling licenses and support for these applications servers so these abstractions were great for their bottom line.

Take away the execution time requirement and injects mocks/fakes/stubs at compile time suddenly you don't need interfaces to make mocking easier, static methods are easy to mock and you probably don't need DI/IOC.

> Java was full of AbstractSingletonBeanFactory classes long before unit testing became a thing

You're right that Java had some long class names to start with, but I think it was mostly Spring that introduced the PerhapsOverlyDetailedClassNamingConvention.

Spring's introduction and subsequent wide adoption was a big driver of TDD's growth, which encouraged all those interfaces and mocks.

Honestly, I like long names style of java. I dislike the code style where everyone is using shortcuts for everything.
I don't mind them if they're not too long. And if you can use more recent version of Java, the new `var` keyword is a nice verbosity reduction.
I think the point of AbstractSingletonBeanFactory is not the length of the name, but what it does. More specifically, on how meta it is what it does.

You can have a bean. Great. You can have a bean that's a singleton. Also great. Then you discover that you can have two different beans that are singletons, and you find that they have one thing in common: how the single instance is created and accessed.

Each such singleton bean will have about five lines of code that deal with the single instance, and these five lines will be largely similar.

So the Enterprise Java Architect sees that there is something to be abstracted, goes to town, and now you can have seven lines of code configuring the abstraction, so that you can avoid writing the five lines of boilerplate.

Unfortunately, the seven lines of configuration will be largely similar for different singleton beans...

> leading to IoC containers, leading to lots of tools to assemble, intercept and rewrite code at execution time.

Yeah, as a (mostly, currently) java programmer this is where the whole "framework" ecosystem loses me.

Dependency injection - absolutely. Really useful, I can have a setup method that reads config from the environment or wherever, sets up various things and passes them in (you need an SSL context associated to these CAs? Great. And you need one that needs mutual authentication? OK then, here's one I made earlier) . Makes testing easier, and makes it obvious where the setup is being done and how this stuff hangs together.

But 'full' IoC with a framework injecting stuff at runtime? Why? This just obfuscates as far as I can tell, and works around the language, making it harder to conceptualise and reason about.

(Also yes, I don't subscribe to interface-itis).

> Unit testing, leading to dependency injection, leading to IoC containers, leading to lots of tools to assemble, intercept and rewrite code at execution time.

The argument for static typing is often so that refactoring is easier and ide's work better.

I always thought it was funny that the core reason for needing fancy ide's and large rename and refactor capabilities was that static typing and over abstraction in the first place.

JUnit and Mockito/PowerMock shouldn't have to exist, at all. The language should be feature-complete enough to provide built-in unit testing with mocking of everything under the sun (including static methods etc) - if you can provide APIs to operate on XML and obscure X.509 formats, you damn well can provide APIs and syntactic support to make modern unit testing possible and well-supported.

As it stands, the necessity to relax members to `public` all over the place drives a giant bulldozer through any remaining delusions that Java is a language where actual OOP can be achieved.

The problem is that unit testing as it is often done tears the object oriented design apart. The right way to do unit testing is to not test the internal state of an object in the test code, but to instead test it within the tested object using assertions. That way, running the unit test implicitly verifies the internal state of the object without having to expose it.
Usually people (ab)use default visibility modifier for unit testing. It's not public, so it's not visible from other packages, but unit tests usually reside in the same package, so you have full access to it.

But I agree that Java could use better unit test and mocking support.

Why use Interfaces for Unittests? Just extend and expose?
The problem used to be the language. Java used to be extremely rigid in the way it forced the developer to write classes everywhere, factories and what not. This and the std lib API as well which was quite verbose. And let's not get started on Java EE which was the recommended way to develop Java applications. All of that are certainly part of the language. Remember how hard and verbose it was to hydrate records from a DB result and turn them into Java objects with pure JDBC? The language certainly was at fault.
> Remember how hard and verbose it was to hydrate records from a DB result and turn them into Java objects with pure JDBC? The language certainly was at fault.

I don't blame the language for this.

It wasn't that hard to read/write objects using JDBC (it did usually create a lot of boilerplate)

Please name a major programming language that has built-in ORM features -- a library is the best place for this sort of stuff.

For Java this was J2EE/EJB (stupidly over-engineered) and Hibernate/JPA (decent if you avoid the footguns). For Ruby you have ActiveRecord, Python has Django and SQLAlchemy, etc.

I think it starts in the language. As a language it is the most bureaucratic one of all. These very deep import paths, the ban on multiple inheritance and the rule that a file can only contain one class all make the language bureaucratic. Programmers willing to put up with that are the more bureaucratically inclined ones and hence one ends up with all of these spurious layers of abstraction, giving every field a getter and a setter and so on.
>As a language it is the most bureaucratic one of all.These very deep import paths... getter/setters

Standard java libraries like java.lang, java.util, java.util.concurrect, java.net, java.io, java.nio etc, hardly suffers from that part.

This is the language - quite straightforward, hardly any getter/setters - relatively simple hierarchy and so on. Then java.awt and later swing, java.bean came with more getters/setters, deeper hierarchies. Those are defunct now of course. I'd blame it on the idea of having visual UI editors to cause the extra fluff.

Then xml, spring and all the friends came. Java got the reputation. Still I'd argue the core java packages don't suffer from deep factory-of-factory jazz. (I avoid most of DI and resent mocks in tests)

Java is quite good in its niche. Java is not a language where you quickly slap some duck tape code. Java is not a language where you use your minimalistic VIM-like editor, it is a great language for quick navigation, great search and making sense of the code with a proper IDE.
It is just human nature actually. Just like only X% of programmers really "get" recursion, only (X/10)% really like/feel OOP and abstract thought in general.
Really really over-simplified, I feel it comes down to a combination of when it was invented and for what reason. Why was Java invented? People wanted a simpler language. What do simpler languages do? Attract more people who aren't willing to put up with a lot of hassle (e.g. memory management, garbage collection, cross-platform). Unfortunately, software dev back then was arguably much simpler (less requirements, less competition) and incredibly lucrative (= people less interested and without affinity come in for the money). The worst seniors I meet are generally Java one-trick ponies, incredibly knowledgeable in Java and the domain of the app, but that's it. Despite most of these apps doing incredibly basic CRUD stuff, they tend to end up more complicated than a freaking game engine, the complete other side of the spectrum.

As a younger dev, it is really difficult to fight against these seniors. Most seniors that care have left, they got the whole market begging at their feet. Other seniors have given up. There's a small number of seniors that still care, and there are layers of bureaucracy between suggesting a change, incorporating it, and seeing the results. It isn't unheard of to have half a year pass by from the suggestion of a change to the first signs of its effect. For many a young dev, that's half a year away from moving jobs without falling under immense scrutiny.

The language is of course part of the problem.

Because having to write de facto one file per class does not really allow you to write very elegant software. It forces you to write files over files and no one wants that, so they fall back to primitive types.

For the abstraction bloat it is actually similar. Here the reason is that for the longest time (and even nowadays) Java makes it painful to work with functions. So now instead of just a plain function, you have to create a class with an interface that has one method... and instantiate it. Great... not.

The problems stem from the lack of top-level functions; everything has to live within a class. In reality, classes (or 'modules' if you prefer) should be low-granularity things in most programs.
I've also seen plenty of horrifically deep layered stuff in PHP, and to a lesser extent JS. Just like Java, it's possible to write elegant well structured code in any of these languages (yes even PHP, even though I still hate PHP).

And of course you can... purely in terms of organizing building blocks most object orientated languages are pretty much a super set. I think the problem is most people are not consciously aware that object orientated approaches are just a pattern, and like any pattern you shouldn't apply them to everything. Being built in to many languages gives the impression they are as fundamental as a loop or a function - those are technically still abstractions, but far more primitive and less subjectively applicable... Object oreanted patterns feel like they are in between domain specific and basic building blocks, the higher you go the more polarizing these become, trying to use a DSL will seem absurd for one thing extremely elegant for another - when classes fit they are beautiful, but when they are carelessly used as a default, they are annoying bags of loosely coupled functionality and data that only serves to obscure relationships.

I'd speculate that the prevalence of poorly applied OOP is more to do with legacy and culture of the language along with the focus on how to use the language for newcomers.

There weren’t a lot of people with experience in OOAD, and all of a sudden we needed an army of Java developers.

J2EE 1.0 came out, and was both over and under engineered (baroque and unworkable, which is ironic given the 7 fallacies come from their employer). And then Design Patterns came out, and it was all over but the crying.

(comment deleted)
One of my dear colleagues a few years ago went on a tear and added interfaces to basically every single class he could lay his hands on. C/C++ header style I imagine. To this day I cannot easily follow code because any time I Ctrl+Click on a method call I end up stuck in the interface and have to first find the single implementing class, open it and then navigate to the corresponding method.
> have to first find the single implementing class

This _can_ be a problem with Java, admittedly. Sometimes you've got a single meaningful implementation, but you need an interface either for using it with services that implement such interface at runtime to create proxies (e.g. Spring, Hibernate, others) that augment its functionality, or to properly mock a dependency. BUT: the IDE should be your friend. Almost every Java IDE supports "go to the implementation".

And, what is the alternative? Fewer interfaces sometimes lead to even worse patterns (for testing: monkeypatching and concrete class subclassing). I prefer the clarity of declaring an interface wherever an interface makes sense.

Using a lot of interfaces and defining APIs (which are, mostly, collections of related interfaces) makes the Java ecosystem highly pluggable. I can mix-and-match a lot of different libraries and/or implementations, as long as there's an interface they can work together in a rather good fashion.

Consider Python: there're very few (and sometimes poorly defined) interfaces and API; so, quite often you need to pick a library/framework which implements everything and works poorly with other implementations.

> I can mix-and-match a lot of different libraries and/or implementations, as long as there's an interface they can work together in a rather good fashion.

But are you writing a library? If not, your product does not have to be plugable. Also if you end with multiple interfaces with only one implementation you should start with classes and extract interfaces when needed.

And I think the Java abstraction root problem comes from the way of doing "unit-testing" at the class level. Your users don't care about your application architecture: they want functionalities. Method and class level unit testing is just fossilizing your code and giving you some kind of feel-good number. You should be able to drop all your code, start anew with a new stack and have your tests still usable. Refactoring should never mean having to rewrite tests.

Imho, organizing code in a braindead way (like single-implementation-interfaces) just to be able to test it is one of the main problems of bad code and bad languages. You make your code harder to understand, more convoluted and less refactorable. The far better approach is to seek out side-effects and state and organize your code like you would in functional languages like Haskell. Then testing is just adding a) testing all the stateless, functional and stateful stuff by pushing through prepared test objects and b) testing the side-effectful stuff through mocked side-effects or a test-mode (which you need anyways for integration tests). Cleaner, faster, easier to understand, easier to reason about. Only problem is that Java is relatively bad to use in a fuctional style.
> But are you writing a library? If not, your product does not have to be plugable.

I'm not writing a libraries, but other people are. In Java I can pick my IoC container, my ORM, my web stack, as I wish.

I can hook-in web-related functionalities through the servlet API filters.

If I pick Django in Python I get a builtin ORM and web stack. Everything is within Django, for Django; extensions are Django-specific.

> You should be able to drop all your code start anew with a new stack and have your tests still usable

In order for that to work, you'll need your interfaces to be stable. It's impossible otherwise :-)

And what kind of interface do your app have? I'd guess a graphical one or some network endpoints.

You should not test code. You should test functionalities.

You should be able to rewrite all your app in a new language and still be confident your tests will tell you if something broke. "Unit testing" at the class level won't do that.

> You should not test code. You should test functionalities.

You're talking about integration tests and/or end-to-end tests. I write those. Usually at the façade level, not at the network endpoint (which usually means the controller level, a shallow layer that often includes some ACL and serialization) but it's just a matter of taste.

> You should be able to rewrite all your app in a new language and still be confident your tests will tell you if something broke. "Unit testing" at the class level won't do that.

Unit tests, integration tests and e2e tests are different things that solve different concerns. An e2e test will rarely expose an obscure bug within an implementation, and a unit test will rarely expose concerns in the global application flow.

Both have their place. And they have nothing to do with the "interface vs not interface", IMHO.

This is hard to work with indeed. But if you eventually want to change program behavior, you just build a class that implements a specific interface and change a config file.
> Ctrl+Click on a method call I end up stuck in the interface and have to first find the single implementing class, open it and then navigate to the corresponding method.

maybe it's a weakness of an IDE?

There is a shortcut that allows you to navigate to the implementation. I know there is one in Eclipse. I'm working on some legacy code where they implemented everything in this fashion (even though 99% interfaces have only single implementation and never will have anything else). This shortcut makes the work manageable.

I don't remember how it is done exactly (it's in my muscle memory), but I think ctrl and hover over shows up a mini dropdown where you can click on what you want to navigate to.

That's something the IDE should handle for you, to pass-through when there's only a single implementation.
I don't know any C++ IDE capable of doing this. Do you have any in mind?
In Eclipse, if you highlight the method call, you can press "ctrl-t" or "ctrl-t ctrl-t" to get list of implementations.

I am 100% sure Idea have some solution too.

Intellij IDEA -

Navigate -> Implementations.

Works on interfaces/classes as well as methods.

Have you tried using an IDE? This will normally let you go to the implementation of a method rather than the interface for it.
which one?
IntelliJ IDEA, Eclipse, etc. I'm sure any non-trivial Java IDE does this.
If the language+IDE combination doesn't allow for a convenient "Ctrl+Click" through the code, then there's a problem. Interfaces, Annotations, "Common" Projects and config files with important constants that are being read at runtime are a form of obfuscation.

We need to be able to bring that up at code review and put it into the style guide.

In eclipse, you can do "ctrl-t" or "ctrl-t ctrl-t" to find implementations.

You can also refactor it back.

I believe it’s called Lasagna Code; too many layers.
"All problems in computer science can be solved by another level of indirection, except for the problem of too many layers of indirection." -- David Wheeler
I think that one part is the architecture. People use patterns to divide up code with duplication and interfaces. This makes the code more complex, but has some merit as the code is more testable and extendable. The second part is the use of object orientation to reduce code duplication. I am not a fan of this. It leads to deep stacks of inheritance where the code is smeared out in many layers. Modular code is much better in my opinion.
> Remember that software is at its core procedural.

> Computers do this thing, then they do that thing, then sometimes some other thing. High-level languages and object-oriented design are fanfare and pageantry distracting from our fundamentally humble jobs: pecking out if-then statements.

Then just use C and reap the speed & ram benefits!

This was the line that made my eyes roll the hardest. Just remember that all cars are at their core internal combustion engines!
Are they not? What's the sole purpose of a car other than to go from point A to point B?
Bicycles are also an excellent means of going from point A to point B, and they're quite a bit simpler than a car.

Internal combustion engines are useful for other tasks, such as powering a generator.

We shouldn't confuse the task with the tool.

Well actually if you're just using the machine as the machine, you should be coding in Assembler anyway. If you're not unrolling your own loops, you're wasting processor time.
There doesn't exist a problem which cannot be solved by adding another iterable enumerator.
I used to be a Java developer now turned mostly to Go and this is one of the things I dont miss at all about Java. So much abstraction and often not for good reasons.

Theres a lot of stuff I miss about Java but the culture of layers is not one of them.

Go has its own set of problems though. There's the oft-mentioned error handling, "type" system and modules.

However, more closely related to the OP, has anyone else struggled with the organization of even medium-sized systems? Obviously some people manage, but I don't know how.

I used to do Go a lot more than I do now, but I think I've narrowed it down to (a) it's simplistic (boolean) visibility capabilities (b) lack of overloading and (c) lack of static functions.

Instead of `User::new()` and `Role::new()`, you end up with `newUserWithPassword` and `NewRole`. You can do a package per module - but that doesn't solve the issue with overloading, and you can get into cyclical imports. You can solve overloading with a config objects, but now it feels like you're back to over-abstracting.

I've worked on a 2-layer warehouse management system written in PL/SQL on the backend with very thin C++ layer in Qt 3 on the front end.

It was pretty nice, there was a proprietary framework to use one XML file per form with SQL and metadata to generate CRUD with SQL-backed grids, filtering, sorting, and simple actions, and everything else was added with straightforward C++ Qt code.

Almost all business logic was in PL/SQL. It was simple and quick and the divisions were obvious.

We moved it to 3-layered Java code with SQL (and some PL/SQL still) on the backend, then Jboss application server and EJB3 code, and Java Swing forms on the frontend.

The goal was to eventually move all business logic from PL/SQL to Java and avoid Oracle Tax (it was before Oracle bought Java BTW - funny how that worked out :) ).

That all would be fine, but our architects bought the cool aid and decided to overengineer the crap out of this software.

What was PL/SQL + 1 XML file + 1 Qt form became like 10 layers of XML files (1 specifying how to get the data from database, another how to transform it to DTO format, another how to represent it on the frontend, another what actions should be possible on it in the frontend, another how to transform from DTO to action object for each action, etc.

Yes you had to write less code in edge cases than in the C++ solution, but instead you had to juggle over 10 XML configuration files even for simplest cases where before 1 SQL wrapped in XML was enough.

It wasn't 3 layers of abstraction - more like 12 in practice. And all that configuration bought you was the ability to potentially write slightly less code when using several forms together and passing data between them (which we almost never did anyway).

It was developed for years and finally shipped (I think), but only to one customer out of dozens and we completely switched the technology again soon after for unrelated reasons.

(comment deleted)
One the worst systems I worked on was tons of untested PL/SQL. Could not change anything without unknowingly breaking something.
It was pretty good code as far as PL/SQL goes. All the functions were divided into packages and followed this template:

    function (inParam1, ..., inParamN, outParam1, ..., outParamN, outMessage): ReturnType 
    begin
        savepoint SP_begin;
        -- do stuff
        
        outMessage := '...';
        return RESULT_OK;
    exception
       when ...
       when others then
          rollback to SP_begin;
          outMessage := '...' || exception details || ' ...';
          return RESULT_ERROR;
    end;
It's funny how I remember this after more than 10 years :)

Made it very easy to add tests and to interoperate with container-managed-transactions in EJB3.

I even added an debug-only interceptor on EJB that created a savepoint before calling any PL/SQL and checked if that savepoint was still valid after the call. If no - that's a PL/SQL that isn't following the template and the developer who changed it needs to be flogged ;)

Recently, for an academic project, I were given a one static one-function very long class that implemented an algorithm (was obviously programmed as a cpp algorithm). The algorithm consisted of steps (gather data, transform data, print data, etc) so as a 'what if' I refactored it by moving each step into a class (so the main function was now one line for each step). Also, what originally was multiple if/else to different outcomes (print to file vs print to console) was now one abstract class with one sub class each. The algorithm code was practically the same, the difference was where it were located. Instead of needing to scroll a lot, you now needed to switch between files.

Was it worth it? Probably yes, not for the final code but for the experience of 'this is an exaggeration that you should not repeat' (although the resulting class diagram was a work of art).

As usual, extreme abstracting is bad, but extreme lack of abstracting is too. You should find a common term that make the current work simple enough, but that also allows for future changes to be simple to implement.

If you are making a list-like class because you need to keep a collection of strings which you are only comparing, don't use strings. Use <T> even if you only use it with strings, the extra cost while creating the class is almost nothing, but the cost of a future refactorization will surely be greater.

"Try not to open doors you don't need, but keep them accessible in case you need them later"

> Use <T> even if you only use it with strings, the extra cost while creating the class is almost nothing, but the cost of a future refactorization will surely be greater.

Allow me to question the "almost" in "almost nothing". Java generics are famously unreified, at runtime it's exactly the same.

I have noticed this habit of what could be called redundant genericity in my own code as well. Sometimes the motivation seems to be hardly more than a coping strategy for the notoriously verbose class names of idiomatic java. But it can also provide self-describing value: at the use site, Bag<String> reads better than BagOfStrings and on the implemention side, class Bag<S> makes it clear that nothing in there will look at e.g. individual characters. Obviously, if you do need to consider individual characters, go with BagOfStrings - or go with class Bag<S extends CharSequence>, which would notify the reader right in the headline that somewhere in the implementing it requires access to the actual text nature of its data.

As a bonus, even if production code only ever fills the angulars with one specific type, chances are that it's a type that is painful in a unit test. If you do esoteric set operations on WhateverFactoryFactories or something like that it's super-convenient if you can just play them through with strings.

> Allow me to question the "almost" in "almost nothing". Java generics are famously unreified, at runtime it's exactly the same.

On the other hand, decaying <T> to Object (rather than monomorphizing as C++ templates do) somewhat reduces the viable attack surface for optimizations.

On the gripping hand, Java is traced/JITted, so it should pick up many of these opportunities (and more) anyway.

> If you are making a list-like class because you need to keep a collection of strings which you are only comparing, don't use strings. Use <T> even if you only use it with strings, the extra cost while creating the class is almost nothing, but the cost of a future refactorization will surely be greater.

Make it a little less readable for a future case you might never encounter?

I'm probably a bit of an outlier, but I do find it more readable to use type parameters instead of concrete types. If the type is opaque, I know the only ways to get or use values of that type are whatever explicit means the specifier of that type provides.

This is less valuable the more specialized the type you're looking at -- generifying everything is just as much an antipattern as anything else -- but String is so widespread and abusable that it really is helpful to abstract over it in many circumstances.

Each to their own, as in so many ways.

To me it becomes effectively another variable I have to track through the code and adds a small amount of extra cognitive load. Not a lot, of course, it's not a huge thing and I do use them where appropriate.

> Was it worth it?

It was - having gone through both extremes you have gained understanding of both and discretion in using either.

I think Java programmers (and their buddies in C# and similar land) can benifit from adapting a straight forward functional style of programming.

Not the stream, map, reduce stuff.

But simply writing functions without side-effects and separating data from functions.

"Remember that software is at its core procedural."

That may be true in some sense. But most Java code is in fact not procedural but stateless in the sense that most often state is store in a database or another external system.

Isn't this a bit the opposite of what the article is recommending? It seems the author would like full oop, stateful objects etc. Or maybe I got it wrong...
I agree and tend to create my code in this style. The bonus to this style of programming is that testing functions in isolation comes almost for free.
> can benifit from adapting a straight forward functional style of programming.

Isn't Kotlin largely scratching this itch?

To be more specific, get rid of setters unless it is a data object. Have all fields be final and set them in one constructor. This makes the code more readable since you know no method will change the internal state.
i thing some clever layering of spring beans makes for good job security. The problem is that you have to reconnect the binary dependencies to the original source versions, but that's an 'implementation detail', isn't it?
There's one point where I'd argue with this - little classes that encapsulate a single simple domain concept are extremely valuable; an example would be a Path type that facilitates building paths, canonicalizing them etc. The key is that they do not add to the cognitive burden, because their behaviour is (should be) obvious from the name and the name of the methods alone. Furthermore, by adding more nuanced type information, they improve the expressiveness and robustness of the code (as opposed to just using a String for everything).

One of my biggest beefs with Java is that, by virtue of the one-class-per-file concept (pace inner classes), the class is elevated to the level of a big deal in the novice programmer's mind, so that a little class with a very clear and narrow scope seems to be unworthy of existence, and we get the String-for-everything syndrome. Anecdotally, I have encountered exactly this attitude on projects.

> a big deal in the novice programmer's mind

That's an interesting take, and I think you're right. In retrospect I have wondered why my more junior coworkers so rarely create new packages or new classes. Perhaps this is the reason. Will have to take that into account when doing reviews.

I'm optimistic that Records will help at least a bit with this problem over time. Simple data aggregates will come with much less ceremony than before. I've always been against generic Pair classes, and with Records there will be such an easy and superior alternative.

https://openjdk.java.net/jeps/359

Not unlike Goldilocks sampling the bears’ home, what is just right, is often a matter of perspective.

A developers education, experience, and domain all impact how different abstractions are perceived. Sometimes having an abstraction can reduce cognitive load. Think of the configure vs convention arguments for frameworks. Knowing how to implement simple functionality the “right way” (within the paradigm of that abstraction) can make building software easier especially when collaborating with others.

I personally find consistency more valuable than having the perfect level of abstraction. Consistency, even if slightly more or less verbose than I might otherwise choose, means I spend less time thinking about the abstraction itself. It allows me to focus on the problem domain, which is where customer value exists. It has the added benefit of simplifying collaboration with others who are also familiar with the abstraction.

It's easy to see Java culture for what it is when you embrace Kotlin which is fully Java-compatible yet doesn't ape the over-abstraction and verbosity of Java. Kotlin releases you from the OOP-only straightjacket of Java. I recently wrote an API with Kotilin/Ktor using only functions and it was a piece of cake. Not a single AbstractProxyServiceBuilderFactory in the whole codebase. Kotlin's multi-paradigm approach makes it an ideal candidate for projects begun in Ruby or Python which need to scale as all these languages are OO-based but with the option of going procedural or functional when required. PHP is weird. After PHP5 was released it's as if a spell was cast on the whole community compelling them to write their code as if the language suffered from the constraints of Java. Talk about culture envy. Procedural PHP seemed to just vanish as if it had been deprecated. So Java culture has infected other languages, notably PHP and C#.
Funny, I write both Java and Kotlin code. My Java code has 0 AbstractProxyServiceBuilderFactory classes. But then again, I don’t follow the common Spring culture.

In one of my earliest jobs, I used to write j2me games. The code had 0 new allocations, mostly static stuff neatly organized in final classes. All of this due to hardware limitations. My point is, the language != culture.

That's fine when you work solo but Java, more than any language, was designed for teamwork and that's where what's idiomatic tends to weigh heavily.
I'm sorry if I implied that I do solo work with Java or j2me. That was/is rarely the case.

But I don't really understand your point about idiomatic. Modern (new) languages, like Kotlin or Go, are less intended for teamwork or less idiomatic? That can't be true.

One of the hardest parts of software architecture, in my mind, is striking the right balance between YAGNI and future-proof abstractions. Abstract everything to death, and you end up with what OP is describing. Abstract too little, and your codebase becomes rigid, hard to test, and so riddled with details that new developers will fail to see the bigger picture.
I would say problem is that people worship DRY like it is some kind of golden rule.

Where KISS, YAGNI and "premature optimization" are left in the corner crying.

Pointing out DRY is easy and during code review all devs know that someone will be nagging about it. Where if you build an abstraction even if not needed or plain wrong there is a high chance that no one will have any comments, because reviewers will go with low hanging fruit and point out DRY violations. It will be more work to find out how abstraction work and point out its weakness, and don't ever even try to propose that code duplication might be a better solution than broken abstraction.

Whenever I can, I try to push rule of 3 for dealing with duplicated code. Unless something is used 3 times it still is not a duplication.

Too little abstraction is rarely a problem. It's easy to change something simple and straightforward. When a bunch of stuff depends on a bad abstraction, it's both hard to change, and hard to keep.
Abstractions, I think, should not be forced. They should evolve, parameter by parameter, as more and more configuration is needed in the original application, until they can be effortlessly lifted to another application. Everytime I've thought: "I'll abstract this so I can one day use it elsewhere" it's ended in failure--a working but overengineered, complex solution that I'd rather leave where it is and start afresh rather than reuse.
I was once told: never make the abstraction to something, until you're ready to make the 3rd instance of it. The reason is, that until you're ready to make the third one, you not only don't know if it will ever really be needed, but you also don't really know which parts of the thing really need to be different/customizable. If you abstract it before then, even if you do end up needing another someday, you will find that the way you abstracted it is the wrong way, and you will need to rewrite all the associated code anyway.

Until you're making your third one (of whatever), don't make an abstraction for it.

Good and pithy advice. I’d expand it slightly: “Until you’re making it a third time, don’t abstract, because you won’t yet know the what or the how.”
That’s sound advice for the day-to-day of programming but I think it still misses much of the power of abstraction. If you choose a different abstraction you might find a completely different way to express your solution which could be orders of magnitude less code.

Perhaps the most absurd example of such a thing I can think of is when people figure out they can replace some gigantic mapreduce cluster with an awk one-liner and have it actually run faster.

Is this related to the "build things that don't scale" motto? That one was conceived for startups in general if I remember correct. Is this same thing but in more detail?
everything there applies as well to components frameworks like angular. most of the angular apps I have seen have more abstraction levels than Russian dolls. Components in Components in Components. It's like the dll hell remade for the browser
It's inconceivable to me that one could write an article on this topic without mentioning Spring and Spring Boot. In many cases, it serves as a pre-built set of over-abstractions, and a great way to burden oneself by forcing all solutions to fit into an abstraction somebody else made. Like most major frameworks, it is undoubtedly convenient for throwing something together, but not conducive to making something well-crafted and maintainable. In my view, it is fine if you keep your code independent of it, but in my experience that rarely happens, and the code inevitably ends up as "a mess that works", and that can't easily be cleaned up.
This is the result of every framework I’ve ever seen and used. It is just more apparent in Java due to being constrained to “OOP.”
Spring works as a maintainable abstraction because it is known well enough within the industry that you can look for, and find, "Spring programmers". The framework's documentation, tests and release notes work to create a relatively coherent layer above the language, which would end being recreated a thousand ways by just as many creative coders if it didn't exist. Most of these in-house frameworks would be undocumented, poorly tested and unmaintained as soon as the lead coders left that department. Even for these standout projects that would be arguably better from a technical standpoint, none would offer the social advantage of a ubiquitous framework such as Spring provides.

The desire to write new code is often so strong that I've seen Spring being itself re-wrapped in a local flavored "frameworks" that only brought value by ignoring parts of what Spring could do out of the box. The solution is most often to strip off the overwrapping and bring the app back to Spring primitives.

Honestly, after using spring and lombok annotation processing, I cant stand Java without them. So much code and functionality gets auto generated, the value is huge. Its just that if you havent worked with them before it looks like a huge abstracted mess.
Exactly, for some reason I worked a lot with Spring over the last 10 years. I refactor any non-annotation based implementation first thing.

But I never gave in to Lombok until recently, I was seeing it as gimmick at best. Like.... Ok, it will generate getters and setters, so can any IDE.

Lombok actually makes writing and refactoring concise java code way easier. Eg: Macro like @builder allow to introduce immutability instantly. Notation of the form Dog.Builder().size(5).name("fido") are accessible for 'free' and are pretty reliable. ( read : you can tweak them if needed without looking a de-compiled code to understand what went wrong )

So, as much as I dislike Java and Spring. I think you are correct. A CRUD microservice can be expressed in reasonably concise way with the method you describe. ( and it's still plain old boring java code running on regular JVM )

Spring sucks but Java Play Framework is great actually.
You can't be a purest OOP or functional developer and also hold high the values of launching early and often (in my opinion). Instead, I think the pragmatic path ahead is codebases that balance and blend these two disciplines well, and largely use the GC (or reference counting/whatever mechanism exists) for managing resource allocation across an entire stack.

That is to say, you intentionally use classes and all the baggage of OOP when you know a part of a given implementation resource and throughput heavy. Otherwise, the default 'starting point' when implementing something is to start with value semantics.

It's fair to say many mainstream languages are adopting this approach. Swift is extremely functional, C# 9.0 adds record types, Go and Rust are largely functional but you can do OOP style things with them.

Java I feel has reached an evolutionary dead end and will be the 90's answer to COBOL.

A lot of these abstractions come from consulting companies that need to pad their hours. I used to think it was very intelligent developers indulging in intellectual masturbation, but I’ve seen it happen too often to be a coincidence: some complex CQRS Mediator pattern on a small project where there will only ever be one implementation, or to facilitate unit tests that add dubious value.

Not surprising in a field where ethics are a distant second to money.

>The new features people ask for never seem to line up with the layers in your code.

This has been my biggest objection to many tutorials, videos, and books on OO, architecture, and patterns: They're useful for greenfield code, but so often in large systems, they are nearly impossible of implementation.

The trouble, of course, is that it's nearly impossible to teach how to do these things inside large codebases that are layered in ways that don't align with the intended change.

Consider that other than Michael Feathers' book, there are few, if any, good books on maintaining code. And so we all struggle, and code becomes progressively more imperative and less coherent.

> They're useful for greenfield code, but so often in large systems, they are nearly impossible of implementation.

This has been true of framework after framework, especially during that era, but we still have them regularly today.