So this is something for my fellow hackers.
I run a successful tech business and I also consult others who want to start their own businesses.
Now this is a problem I'm seeing other tech CEOs start doing which is use AI to have the code written for them by either using some sort of editor plugin or by using something like Cursor.
The problem is that the code that is generated is usually humongous. There is just huge amounts of types and indirection and functions, calling functions and doing all sorts of nonsense which can be done manually, much simpler with fewer lines of code.
This is creating huge amounts of AI-generated slop.
Now when I come in to consult some of these tech architectures it takes me a really long time to figure out how to improve things because there is so much of indirection in the code base and the more troubling thing is that there are errors hidden in the architecture which earlier I would easily find out, but now it requires me to go through every single line of code.
In some of the worst cases, there have been bugs which have taken down their system and then they come and blame me for not being able to find it.
This is the same problem that I had with Java shops. A lot of Java programmers immediately start using a lot of classes and objects because they had for a very long time superior tooling and IDEs.
My theory is that Java is actually a pretty reasonable good language but because there is such easy tooling and easy autocomplete you can almost always increase the huge web of classes and objects because you can immediately reach for them by pressing a dot.
Now take that and increase it like 10x with all of this AI generated code. The medium is what influences what the code generated or written is.
So how are you all handling this problem? Do you find this to be a big problem in the code bases that you see?
It's also hard to tell them not to use AI because the code does work. I would say even most of the times the code does work.
But it's just written in the worst possible manner and maintaining it long term is going to be so much harder if instead they had just handwritten the code.
> mostly written by junior engineers or even students (because there are so many publicly-accessible Java school-project personal GitHub repos.)
I used to work at a Java shop a few years ago. Hated my time there since a. I was coming from a more pragmatic way of writing software in C and Java and its ecosystem made me pull my hair out and b. came across several very senior engineers who insisted on writing code with factories and builders and obscure design patterns which made no sense to me. They were all straight from praying at the altar of Design Patterns and always found a way to give comments about a future scenario where this would be refactored.
I was fairly convinced that design patterns in OOP languages were conceived by relatively senior folks as a form of gate keeping.
(rant incoming)
I wish C was better. A company I have to interact with at $work managed to involve 10 different processes just to change wifi network name (it all boils down to a single netlink message to the driver). And eventually there were too many daemons so they replaced the entire thing with a new architecture where everything ran in one process but with many threads, with no regard for thread safety and blindly copy pasting hostapd code all over the place.
Everything is passed and parsed as a string, so you get none of the advantages of C (speed, simplicity) and all of the drawbacks (ABI breaking, etc.). To make matters worse, these people apparently haven't heard of databases or even plain text files. Data is stored in XML or JSON "databases", but of course having a full XML parser is too expensive so it's actually their own homemade XML parser which requires line breaks in certain places and literally does strchr('<').
The funniest thing was when they replaced dbus with their proprietary data bus (with zero debugging capabilities of course) because dbus was "too slow", meanwhile at the same time their "hashmap" is actually a linked list (!!!) because they thought it would be premature optimization to write a proper map type. All written in C and yet somehow slower than python.
I'm really looking forward to the stone age of programming, because we're definitely not there yet.
While rooted in actual use cases, I agree that often the GoF and other patterns are applied when there’s no need. My guiding principle nowadays is deletability/unpluggability - structuring things in a way that removing a component or behavior is as easy as possible. I do need the same patterns to achieve this, but I don’t blindly apply them, and use them way less as a result. Eg, with Swift‘s value types and great closure support, I can often go 90% of the way just with structs - no interfaces (protocols) or classes needed.
> came across several very senior engineers who insisted on writing code with factories and builders and obscure design patterns which made no sense to me.
In a large-enough Java codebase, with good software engineering being done, you will eventually hit the problems for which these weird kinds of indirection are the solution (because Java is dumb and forces them) — but it tends to take a while, and not involve concerns that are the first priority in a greenfield project. I'll just address the factory-pattern here, but the builder-pattern has a similar story.
The Java factory-uber-alles mode of thought (i.e. "make all your Java classes factories; and all your static methods, including constructors, into instance methods on those factories") exists because doing any kind of sum-typing on Class objects in Java is painful.
Java has interfaces. But Java has no concept of type-level interfaces.
In theory, Java could have:
1. extended the definition of interfaces, to validate that certain static methods and constructor signatures exist within the Class objects of implementing classes;
2. and then introduced something like a ClassWithInterface<T> type, that a Class object could be cast to if-and-only-if that class (or one of its ancestors) implements that interface.
But Java never did this.
And even if it did, by definition, a Java interface must still be known to the class defining it, and the membership of the class in the interface declared explicitly by the implementing class.
Which means it's a bit too late to make this change now, for it to ever have any kind of useful impact — there's so much existing Java code that can't be changed [old stable JDK versions everyone depends on; third-party libraries that are basically abandonware; etc], which would therefore not have defined membership in whichever later-defined type-level interfaces these types really should have type-level membership in.
And even if this were part of Java from the beginning, you'd still have the situation where you want to "make" a third-party class part of a type that the maintainer of that class doesn't think it should be a member of. (Maybe because that type-level interface is a private one you're defining within your app!)
With type-level interfaces, you could put a Class object into a type-level-interface-typed variable, and then use that variable to invoke the static methods of that class, construct instances of the class, etc.
But because you don't have type-level interfaces, to interact with a Class object "generically", you instead have to resort to using the java.lang.reflect package — which, depending on the method, either 1. accepts and returns plain Objects (resulting in extremely error-prone code); or 2. requires that you gradually find the right method handle (and catch five possible exceptions); pack the parameters for calling it (and catch five more possible exceptions); call it (and catch five possible exceptions besides the ones the typing yourself); do any return-type coercion yourself (three more exceptions)... and where all of those exceptions are extremely opaque and obscure the "actual" error (e.g. the caller passing an argument of the wrong type.)
Which means that any time you might want to take code that calls a static method on a Class, and instead make that code call a static method on the value of a Class-typed variable... you're now in for a world of hurt. Dependency injection? Mocks in unit tests? Strategy/adapter patterns? Plugins systems? All become the hairiest Java code in the world.
...until eventually you get fed up, and realize that you can have type-level interfaces today, by just taking the statics and constructors on these classes (that you may or may not own); creating equivalent wrapper instance methods on little proxy classes you do own; and then make all those little proxy classes implement an interface. Bing-boom-bam, you now have a "type-level-interface"-typed variable (i.e. a variable typed as the regular interface type implemented by these proxy classes), that you can (through these proxies) invoke these static methods and constructors on.
What do you call these little proxies only intended to wrap statics and constructors? Well, how about "factories", since statics+constructors tend to mostly be used to make instances?
I think you misunderstood the OP; they were comparing two different conditions (Java slop vs. LLM slop), because they think there's a common cause (tooling lets people make overly complex systems that still kinda work but are difficult to understand).
As far as I could tell the OP was not saying, at all, that the LLM slop was written in Java.
> LLMs are always going to generate output (prose or code) in the same style as the training data.
There's also an implicit bias towards blathering in LLM output, especially in open-ended scenarios like code completion. Language models prefer high-probability outputs; if it's appropriate for a model to generate highly stereotypical content like boilerplate getters/setters and documentation comments, it's more likely to generate that than something more application-specific like the actual implementation of an algorithm.