Sounds like you haven't really understood the benefits of polymorphism, encapsulation etc.
The general idea is to compartmentalize your logic and only expose the important parts like if you need to generate pdfs from html you may need to use a lot of moving parts to make that happen but you hide all of it and only expose an interface with a single action that takes html and returns a pdf. If you later need to change how you do the pdf generation you can freely mess with the internals of the system any way you want because consumers have no dependencies on any of it. If you need to rewrite it from scratch you can implement the same interface and none of the code using it needs to change at all.
I disagree with the suggestion that parent poster doesn't understand the benefits of polymorphism and encapsulation.
Polymorphism and encapsulation are perfectly achievable without the programming practices being criticized by them.
I don't think any sane developer would complain about having exchangeable/reusable HTML/PDF/??? generators. Funny enough, "PDF Generator" is also a very popular example of a "sane" micro-service.
The real problem starts when every single class in the application is given this treatment, even in the cases it doesn't make sense to make it replaceable. This problem is compounded by having very small classes that don't do much by themselves and have (hidden) dependencies with too many other classes. That's how you get the FactoryFactoryFactory problem.
Not everything needs a hyper-abstracted solution. Most people suck at writing neat abstractions, so they spend an excessive amount of time writing shoddy solutions which no one really wants to deal with but there's a soft, begrudging agreement to use them anyway while the business sees no value in having their devs rewrite it. This goes double when you step into the world of microservices, where you now have another layer where you can easily swap the implementation behind the interface.
Surely years of old Java shops with absolutely dreadful software architecture, sloppily copied from literature, showcase the path to hell being paved by good intentions.
But factory is not "hyper-abstracted". For that matter, factory factory can be annoying, but still does not require all that much abstract thinking to understand.
It would be different if people complained about actually complicated stuff ... but, you know, factories are not difficult to understand. Even when I disagree with design decision to create a factory, it is clear what factories do.
Sure, you can write bad code using abstractions. Doesn't mean abstractions are bad or useless. There's just a lot of devs out there who don't write very good code.
There are also devs writing decent code but too tightly coupled, making it much more difficult to develop the code further without first refactoring the existing solution. It is also often very difficult to refactor the solution because its parts are also very tightly coupled so you end up with a whole tree of changes you have to make before you can do the actual refactor you want in order to make the actual change you wanted to do in the first place.
Eh. It’s usually not that hard to take two tightly coupled components and insert an abstraction layer in between. Usually tightly coupled code is small, and you can almost always use the compiler to find all the places you need to modify things.
Practically speaking I’d much rather that problem than it’s inverse. It’s exhausting dealing with code that’s too abstract. Especially if the abstractions don’t do anything anyway. Eg - Java interfaces with exactly one implementation. Removing useless abstractions never feels pressing enough to do, but you pay a tax for those abstractions constantly. And it doesn’t take many abstractions before your productivity plummets. No thanks.
The best code is like the best writing. Say what you mean. Be direct. And be humble enough to redo it all when you figure out a better way to proceed.
Two? Sure. How about when the whole application is tightly coupled so a change in one class necessitates changes in 4 others? And sure if the logic is straight forward and simple that's fine, but add in that the code is a complete mess with a bunch of weird convoluted solutions to things and whoever wrote it is long gone so nobody knows what it's supposed to do beyond "exactly what it currently does".
I think I'm pretty good at programming but that's generally what I've been doing in my career and to me it's incredibly difficult. Not that much new code, just a lot of wrangling shitty old code and in retrospect regretting that I didn't just delete it and write everything from scratch.
Bad code is still bad code. And correctly abstracting everything is still the ideal. Abstractions are like hinges. Too few and everything is a big fused mess. Too many and your system is a wobbly mess you can get lost in. You need the right abstractions in the right places.
For my money the way to do that is to start with as few abstractions as possible and only add them in the places you need them, when you need them. Start simple and refactor as you go. If you try to predict where the abstractions go before you’ve designed your system, you’ll get it wrong and then you’ll either leave your code with bad abstractions or spend far longer than you had to refactoring. (Since refactoring is harder the more code you have).
I’ve mostly worked in Microsoft shops. And I used to not like that. A lot of the guys around me adopted the attitude of, “what does Microsoft have that does that? “. As I’ve gotten older, I think that Microsoft has enough churn of their own. But it’s far less than opening up to the wild west that is GitHub entirely.
I’m pretty grateful. Everything I work on is either C# or JavaScript. And I do like the abstractions and architecture that comes out of that space. It’s probably similar to a Java shop that way.
A lot of the comments here sound like people are really hacking away with some short-lived library, not framework, off of GitHub in some random language that no one else on the team knows, and now the company will have to hire for forever. Or rewrite. That doesn’t sound like fun to me at all. I definitely wouldn’t build my own business on it.
I work the same way now and I completely agree with you. MS stuff isn't perfect and if they don't provide what I need I generally find the most popular thing on Nuget or roll my own.
Meanwhile in JS land things are all over the place. My last team had multiple apps using all of the following and more: JQuery, Vue, AngularJS, React and Svelte. Design was done in Patternlab but had to be manually copied over and translated to cshtml and some parts were done in one of the aforementioned Js frameworks.
To me that was a nightmare. I think most stuff didn't need any JS frameworks at all, and trying to understand pages rendered partly in cshtml and partly in a Js framework is terrible, especially when they're all different.
I think the simpler solution is to design the code around some kind of pdf generation library that is linked to, instead of making a heavily abstracted mini-framework to squat on the application.
Yeah that's one of those aforementioned "moving parts". You design the interface with a single action taking html and returning pdf. That way it doesn't matter how you change these moving parts, you can replace this pdf library easily and as long as you find another way to make this thing take html and return pdf you're golden, none of the code using your pdf generator ever needs to change no matter how much you need to change this service. Unless you change its entire purpose - converting html (and maybe other stuff) to pdf - none of its consumers need to change.
That's the idea. You define a clean and well-defined interface beyond which everything is hidden. This keeps the code simpler because you can focus on one little part and know nothing you do will change anything anywhere else. Its purpose is well defined.
As an example of a less well defined service I'm currently working on a class DiffService which has a method which gets data from a DB, then it gets data from another DB, then it gets data from an API and then it checks for differences between the two db datasets and uses the api data in the process somehow, I can't remember atm. In my opinion there should be one class where all the data is put together, then it should send the data into a different method in a different class where the only concern is taking the data and combining it in the required way.
This way the actual logic is separate from the data collection. It sucks trying to test this logic because I have to mock a bunch of different services and then test the logic. If the concerns were separated I wouldn't even have to unit test the data collection service - it's just calling some data collection methods and sending the data on to this new service which I could then easily test without mocking anything. I might also separate the part with that api call to yet another place if I can. That way I can have a completely dedicated DiffChecker with super simple and understandable logic, a SomethingDoer which takes that api data and does whatever it needs to with that, and a more general service class tying all these parts together. I can also use these parts in a different part of the code that does almost the same thing instead of having the logic duplicated.
I don't really need any new interfaces for this refactor - abstraction isn't exclusive to interfaces. The code will be more spread out so that does take a toll in terms of mental load but I think that is alleviated entirely by the simplicity of the new components - DiffChecker checks for diffs in provided data, it doesn't do anything else. SomethingDoer does some specific little thing I can't remember at the moment but it'll be equally simple in principle. DiffService ties the whole system together and provides an easy way to get the difference - just like it used to before. It'll just be easier to test, the new tests will be clearer which allows them to more clearly convey the purpose of the code they're testing, the new code will be reusable and allow me to easily perform a similar refactor of a similar system and reduce the total amount of code.
Some people will probably say I'm overdoing it, I think they're wrong. I think a class with one method that does one simple thing is extremely easy to conceptualize, allowing you to stop worrying about what's inside it. Meanwhile the existing solution is extremely difficult to completely wrap your head around. It gets all this complicated data, then it does multiple different operations on it before it returns a result. I have worked with this code for a bit, I've read this method at least 20 times trying to fit everything in my head and it's a struggle. It may sound simple but every part has complexity and nuance in the way they're put together and it adds up. To me it becomes much easier to fit everything in my head when I compartmentalize it into small simple parts that are easy to understand and are put together in a clear and cohesive way.
var dataA = db.GetDataA();
var dataB = db.GetDataB();
var diff = diffFinder.Diff(dataA, dataB);
var apiData = apiClient.GetData();
var enrichedDiff = diffEnricher.EnrichSomehow(diff, apiData); // This would have a more descriptive name
return enrichedDiff;
This is what I want my code to look like. This is already plenty busy, there's a lot of stuff happening here. It'll also have logging. Imagine trying to read it if it was 100+ lines because everything was written directly in this one method.
The general idea is to compartmentalize your logic and only expose the important parts like if you need to generate pdfs from html you may need to use a lot of moving parts to make that happen but you hide all of it and only expose an interface with a single action that takes html and returns a pdf. If you later need to change how you do the pdf generation you can freely mess with the internals of the system any way you want because consumers have no dependencies on any of it. If you need to rewrite it from scratch you can implement the same interface and none of the code using it needs to change at all.