logoalt Hacker News

foobarkey01/22/20252 repliesview on HN

SOLID and clean code are not some universal bible that is followed everywhere, I spend a considerable amount of effort reasoning juniors and mid levels out of some of the bad habits they get from following these principles blindly.

For example the only reason DI became so popular is that you could not mock static in Java at the time. In FB codebase DI was also used in PHP until they found a way to mock static, after which the DI framework was deprecated and codemods started coming in removing DI. There is literally nothing wrong in using a factory method or constructing what you need on demand. These days static can also be mocked in Java and if you really think about it you see Spring Boot adds a lot of accidental complexity (but sure its convenient and well tested so its ok to use), concepts like beans and beanfactories are not essential for solving any business problem

Which brings me to S in SOLID, which I think is probably top 2 worst principles in software engineering (the no 1 spot goes to DRY). Somehow it came from some early 2000-s TDD crowd and the test pyramid, it makes sense if you embrace TDD, mocking, test pyramid and unit tests as a good thing. In reality that style of software is really hard to understand, every problem is split into 1000 small pieces invoking each other usually in some undefined ways, no flow can be understood without understanding and building a mental model of the entire 1000 object spaghetti. The tests themselves mostly just end up setting a bunch of mocks and then pretty much coupling the impl and the test on method call level, any change to the impl will cause the tests to break for only the reason that the new method call was not mocked. After going through all this ceremony the tests are not even guaranteeing the thing will work during runtime since the db, kafka or http was mocked out and all the filters, listeners, db validations were skipped. In these days so called integration tests with docker compose are a lot better (use actual db or kafka, wiremock the http level), that way your have a reasonble chance to catch things like did this mysql jdbc driver upgrade broke anything

I have to mention DRY also, the amount of sins caused in name of DRY by juniors is crazy, similar looking lines get moved into a common function/method/util all the time and coupling is introduced between 2 previously independant parts of the system. As the code involves and morphs into something different the original function starts getting more args to behave differently in one case and differently in another case, if it had been left as separate files each could evolve separately. I dont really know how to explain this better than coupling should not be introduced to save few lines of typing or boilerplate, in fact any abstraction or indirection should only be introduced when its really needed, the default mode should be copy/paste and no coupling (the person adding a cross cutting PR will likely not be a jr and has enough experience to know how and when to use grep).

Anyhow I have enough experience to know people are usually too convinced that all this solid, clean code stuff is peak software so I wont expect to change anyones thinking with 1 HN post, it usually takes me 2 years or so to train a person out of this and back to just putting the damn json in db without ceremony. Also need to make sure LLM-s have some good data that is based on experience and not dogmas to learn from :)

As for L, no strong beef with L it’s OK


Replies

Fuhrmanator01/22/2025

> sins caused in name of DRY by juniors

A discussion of clones that can be OK and more: <https://cormack.uwaterloo.ca/~migod/papers/2008/emse08-Clone...>

unscaled01/22/2025

The rationale for Dependency Injection was never _just_ about "making testing static methods" easier. In fact, Dependency injection was never about static methods at all. No DI advocate — not even the radical Uncle Bob — will tell you to stop using Math.round() or Math.sqrt(), even though they are static methods.

The main driver for dependency injection was always to avoid strong coupling of unrelated classes. Strong coupling can be introduced by cases like Class A always instantiating a class B which is a particular subtype of class S (i.e. giving up the Liskov substitution principle), Class A initializing class B with particular parameters that cannot be extended or overridden, Class A calling a static method or a singleton method which modifies or reads a global value.

Strong coupling makes you lose on flexibility, reusability and code readability. If you need to modify how either class A or class B behave later, you may now need to painstakingly scan all the places BOTH classes are used (and all the places other classes touching them are used) and modify the way they are constructed. If you want to enable OrderProcessor to accept bank transfers, but it was built to always call "new CreditCardProcessor()" internally inside its constructor, you will now have to find every place CreditCardProcessor is constructed and modify it. The worst offenders I've seen are pure logic classes that have no business having side side effects, but still end up opening multiple files, or doing a bunch of HTTP requests that you cannot avoid, because their authors just thought: "Cool, I can mock all this stuff with PowerMock while testing!"

The other issue I mentioned is code readability. This is especially an issue with singletons or static methods that mutate global state. You basically get the dreaded action-at-a-distance[1]. You might initially write a class that is using a singleton UserSessionManager object to keep track of the current user session. The class only operates on simple single-threaded scenarios, but at one point some other developer decides to use your class in a multi-threaded context. And Boom. Since the singleton UserSessionManager wasn't a part of the interface of your class, the developer wasn't aware that it's being used and that the class is not ready for multi-threaded contexts[2]. But if you've used DI, the dependencies of the classes would have been explicit.

That's the true gist of DI really. DI is not about one heavyweight framework or another (in most cases you could do it quite easily without any framework). It's also not a pure OOP technique (it is common used in functional languages too, e.g. with Reader Monad). Dependency injection is really just about making your dependencies explicit and configurable rather than implicit and fixed.

As a tangent, mocking static methods was possible for a rather long time. PowerMock (which allows mocking statics with EasyMock and Mockito) was available at least since 2008, and JMockit is even earlier, available at least in 2006[3]. So mocking static methods in Java has been possible for a very long time, probably before even 5% of the Java programmers have even started using mock objects.

But it's not always ideal. Unfortunately, tools like PowerMock or JMockit static /final mocking are working by messing with the JVM internals. These libraries often broke down when a new version of Java was released and you had to wait until the compatibility issue was fixed. These libraries also relied on tricks like custom classloaders, Java Instrumentation Agents and bytecode manipulation. These low-level tricks don't play way with many other things. For instance, if you are using a framework which needs its own custom class loader, or when you're using another tool which needs bytecode manipulation. I was personally bitten by this when I wanted to implement mutation testing[4] in Java, and I couldn't get it to work with static mocking. Since I believe mutation testing carries more value than the convenience of being able to mock statics for testing, it was an easy choice to dump Powermock.

[1] https://en.wikipedia.org/wiki/Action_at_a_distance_(computer...

[2] https://testing.googleblog.com/2008/08/by-miko-hevery-so-you...

[3] http://butunclebob.com/ArticleS.MichaelFeathers.ItsTimeToDep...

[4] https://en.wikipedia.org/wiki/Mutation_testing