3 ms·
I agree "high cohesion, loose coupling" is a good architectural property to strive for, but OOP is terrible at it and there's no reason to use it for this. Try
by sirwhinesalot 4mo ago
I agree "high cohesion, loose coupling" is a good architectural property to strive for, but OOP is terrible at it and there's no reason to use it for this.
Trying to achieve "high cohesion, loose coupling" in OOP has led to the creation of (supposedly) best practice recommendations like the SOLID principles, and the development of monstrosities like dependency injection frameworks.
If OOP was actually good at "high cohesion, loose coupling", you wouldn't need them.
- KayEss 4mo agoI think this is looking at what OOP has become (as implemented by systems and programming languages that don't care a wit about what OO was meant to be) rather than what Alan Kay described. If you think about something like a web server as an object, it has arbitrarily high cohesion and arbitrarily low coupling. You can only communicate to it through messages (HTTP); binding happens at the point in time that the message arrives (notwithstanding that this may be cached in all sorts of interesting ways in the implementation of any given server); and the web server is fully encapsulated (security flaws notwithstanding). I think it's perfectly reasonable to argue that much of what gets called OOP doesn't deliver on the promise, but then it doesn't deliver on the premise either, and I think these are inextricably linked.
- sirwhinesalot 4mo agoThe Alan Kay variant of OOP is superior in this regard, and I'd argue the Erlang variant even more so (Actors are just asynchronous objects). But both of them are still suboptimal because they allow objects to instantiate one another and to directly communicate with whomever they come in contact with. That creates strong coupling and you have to "fight the paradigm" to avoid it. Other engineering domains have this figured out: components can only send and receive data through ports, they are never aware of their siblings. Only a component at a higher level of abstraction can decide how the ports of lower level components connect to one another. That completely eliminates coupling. (You can of course replicate this type of architecture with OOP, but you can also replicate it with any turing complete paradigm, that's neither here nor there)
- Folcon 4mo agoWhen you say other engineering domains have figured this out, can you give a more specific example?
- sirwhinesalot 4mo agoYes, for example if you are doing any sort of mechatronic systems engineering (cars, planes, rockets, etc.), you're typically using tools like Simcenter Amesim or Modelica or Simulink/Stateflow to digitally prototype the system. These tools are all oriented around blocks with ports. They receive data on their input ports and emit data on their output ports. They do not know where that data is coming from nor where it is going. They cannot instantiate other blocks. To create a new block you either create a primitive one (described directly in terms of differential equations, since these are physical simulations, or as actual code, be it C or MATLAB or whatever else), or you connect blocks together to form a larger block. This creates a strict hierarchy, connections between blocks can only happen at the same level of abstraction. You can just rip out a component and replace it with another with the same ports, or with multiple components each filling in part of the job, or you can plop another component in the middle that's only used to process the data inbound and redirect it somewhere else. This makes coupling extremely loose by design. The paradigm enforces it.
- saint-evan 4mo ago'in->func()->out' work cleanly for physical engineering domains because blocks of computations (or functions) have no memory basically. There's no DB and there's usually no global state in the way you'd mean with classes. I've observed that when you work on complex software, you could play by the ear to enforce 'high cohesion and loose coupling' up to the point to where you've now got a data store holding system state. Based on what I've worked on, this system state is actually user data and then some derivative of the system's own interaction on this user data, both of which are necessary for the 'continuity of system runtime'. For example, working with redis as the primary data store, I extracted different types of redis calls for specific keys, (like set [specific_key], get [specific key], etc.) into functions, then I put these functions into a StateStore class so I can simply call StateStore.get_user_data() or StateStore.set_user_data() etc from any module across the entire system. From first principles, this is great modularity and high cohesion but it's very tight coupling around a single module i.e. StateStore. Any change in that module means I'd have to find all references for that updated function and cross check for any contract violations. It's difficult for me to see how software can purely be 'High Cohesion and Loose Coupling' regardless of paradigm or architectural pattern. There's always gonna be an unavoidable and inevitable principle violation that's simple a result of a large complex system actually doing many little things that come together to actually do one big thing. The software itself, no matter how complex, IS the blackbox where data goes in and something comes out but that's the user perspective not ours as the programmer. I think the idea I'm trying to hint at is that software cannot have every module independent of every other one. That's impossible. If nothing depended on anything else, the system wouldn't do anything. Rather than have every module know redis keys, States tore actually concentrates coupling into one place, which is exactly what high cohesion tries to accomplish. I think that's the idea I'm chasing.
- Kinrany 4mo agoI dislike OOP as much as the next HN commenter, but dependency injection tools are good in principle. OOP just uses them much more and for bad reasons.
- bluGill 4mo agoOOP isn't why they are used. OOP may enable injection more than others, but it is not encouraged. Bad programmers and bad architects is why it is overused not OOP. Keep the blame in the right place.
- sirwhinesalot 4mo agoDependency Injection frameworks are a workaround for limitations of the paradigm, much like Design Patterns also are. They're necessary (by a very stretched definition of necessary) because people want to do certain things and the paradigm is fighting them. Some part of the code wants to do logging. The paradigm-native solution is to just instantiate a logger object directly, but that's not really what you want to happen, because there are application-level concerns as to where those logging messages should go. So instead of a direct new, you create a Logger interface (extra work), then use the Factory pattern (extra work) to hide the concrete logger objects that get instantiated. Ok, but now you are still creating multiple independent loggers, which will trample on each other. So you employ the Singleton pattern (extra work), but now you can't use different loggers for different parts of the codebase. Ok, let's instead have the application instantiate the proper loggers, and then thread them through method arguments as needed (extra work). But now passing all those loggers around is super annoying and extremely noisy, so you create a framework that can inject them where they are needed. Great, now you have a whole framework just to pass some logger objects around. The framework is not the problem per se, it exists for a reason, the problem is that someone felt the need for it. And it is a massive pile of complexity that is NOT worth it.
- Kinrany 4mo agoIgnoring the singleton for now, because I don't quite see the need. What's the alternative? Which part of this is actually caused by OOP? If you want to isolate IO, which you should, then you need to inject things that do IO and you need to specify a contract for them. And then it's nice to instantiate things or call functions without explicitly instantiating the whole tree of their dependencies.
- socketcluster 4mo agoThere are definitely cases where you could have a module with distinct responsibilities; so you can definitely get high cohesion and loose coupling without OOP, that's true, but there are cases where you may want to: - Control the timing of when a module is activated (instantiated). - Have multiple instances of a module with variations in functionality where those variations are not a concern to the parent module/instance. For me, this is when OOP becomes most useful. If I can write some code once and later use it to create any number of independent instances with the same functionality which can clean up after themselves, this is generally a lot more maintainable than having one module to keep track of all the different states in an array and micromanaging (for example) the rendering and cleanup work associated with multiple distinct pieces of state.
- sirwhinesalot 4mo agoTo be clear I'm not arguing against objects here, I don't disagree with anything you wrote. I'm only arguing against the somewhat commonly held idea that OOP helps with loose coupling. Interfaces (a feature of OOP) can be used to decrease coupling, but the paradigm itself doesn't really provide any assistance in regards to coupling. The default is for objects to directly instantiate other concrete objects and to be able to directly communicate with any object they at any point come in contact with. That is strong coupling, by default. You have to do extra work to get looser coupling. Smalltalk style OO is much better (regarding coupling) because you don't have explicit interfaces, you can always replace an object with another. You can also query all live objects of a certain type and replace all of them with a proxy if you so wish. Way better. But you can go even further in regards to loosening coupling. You can have objects communicate through a tuplespace (Linda), you can have objects receive and send messages only through ports, you can have objects send a message to their implicit parent who is then responsible for redirecting it. Mainstream OOP is a very poor paradigm full of issues that gets mogged by everything. Composition-over-inheritance is an admission of defeat for a paradigm that lacks native delegation support (good on Kotlin for realising this). It even gets mogged by languages like Haskell that introduced the more loosely coupled idea of typeclasses, which were inherited by Rust (traits), Swift (protocols), Go (interfaces, not to be confused with declaration-time interfaces as in Java). We dug a suboptimal hole and stuck our head in there for 50 years.