3 ms·
"What is the OOP point of view missing here?" The problem isn't that it is missing something but that it has extraneous parts. Inheritance as your default comp
by jerf 2y ago
"What is the OOP point of view missing here?"
The problem isn't that it is missing something but that it has extraneous parts. Inheritance as your default composition method couples together subtyping and interfaces. You basically can't build a modular monolith out of a large class hierarchy because the very act of being a large class hierarchy means you have more coupling between your classes than a modular monolith permits and already have a just-plain-monolith. From there standard code entropy will only grind it in harder.
You really need to eschew inheritance for a good modular monolith and base it around interfaces only. Every major bit of functionality in your system needs a cleanly specified set of services it depends on, where that specification is just an interface and not hard-coded to some specific type, and certainly not some sort of class where subclasses must not only fulfill the interface but conform to the Liskov Substitution Principle, which is much, much harsher than most people realize.
Then you have a true module, where if one day you need to yank out a particular service and make it a true microservice, you "just" take the interfaces it needs and either carry along the services locally or run them over the network. I scare-quote "just" because it's not infrequent to need to tweak the interface to permit it to work over the network (more things that can fail), but it's still a relatively mechanical and feasible process versus taking a whole bunch of super-hard-coded specific types, accessed randomly through whatever language scoping mechanism made sense at the time, and trying to convert that to run over the network.
This is just a summary, of course, because it's an HN post and I can hardly lay out a complete design philosophy to the n'th degree in a comment. But there definitely is a true distinction between a monolith with all sorts of hard wiring internally and a way you can design a modular monolith that is still a monolith, but the coupling between the modules has been slimmed down to a minimum and controlled through some consistent gate rather than just willy-nilly wired together.
Or, to put it another way, it's the difference between the module directly loading another module to directly resolve DNS, directly using the filesystem, directly wiring in a specific type for user authentication, and using a global instance of a logging system instance as initialized by some other module, you know, basically how most people write code all the time, and a module that accepts "a thing to resolve DNS addresses", "a filesystem interface", "a thing that authenticates users", and "a logger" where each of those things are interfaces, minimized down to just what the local module actually needs. One is a coupled nightmare no one want to touch. The other is easily swapped out to work with S3 to store files instead, or inject a different authentication system that ends up using a network service rather than directly hitting the DB, or swap in a hard-coded DNS resolver for testing purposes to avoid dependencies on the real network, etc.
It isn't even really anything that surprising. A lot of people will at least claim this is just "good programming". But you don't get the benefits if you don't actually do it.
- alganet 2y ago"True" modules rarely exist in real life. Maybe toys like Lego bricks. Even those have a lot of mismatch sometimes. Sometimes we want to build something that the toymakers never imagined. Pipes and tubes require glue, engines require lubrification oil, bricks require cement. Lots of non-modular things holding stuff together. But we want true modules to exist in programming, right? Why not remove all the glue and oil and messy cement? Make it _truly_ modular, everything fitting together perfectly. I've done plenty on both sides of the argument. Had my fair share of trying to square the circle, only to end up chasing an infinite problem. Increasingly monstrous interface sets, contradictory names, things so abstract I needed a map to understand them. Sometimes using a drop of glue was better than all of it.
- jerf 2y agoRequiring numerous large interfaces is a True Truth about your code. You didn't make anything go away by just giving up. You just forstalled any other future options. The complexity was there regardless. I suspect it is likely you did not do the interface minimization, which I didn't have time to go into. You should not hand "a file system" to your code when it does not need it; if all it needs is to "read a file by file name" then the interface it receives should have only that. If you have the occasional module that takes 25-50 inputs, I just pay the price for that particular module. If every module in your system is taking dozens upon dozens of inputs, you aren't building a "modular monolith", you're just building a monolith with extra random cuts in it, which is the worst of both worlds. You can take an architecture diagram and drop an arbitrarily complicated line across any bit of it. The resulting code will also be arbitrarily complicated, but you can do it. (See also what microservice architectures tend to become anyhow.) But if you keep dropping lines across your architecture and they're all really, really complicated, the problem is your procedure for drawing those lines, not the utility of a separated architecture. This is one of the reasons I'm not a big fan of rigidly following "clean" or "hexagonal" or any other particular architecture, because even as they have good ideas, rigidly following them inevitably involves drawing overcomplicated lines across the architecture. They're useful ways to get started but true design taste requires an inexpressible understanding of where to cut things that none of them can hand to you as a rigid recipe.