Does composition violate the D in SOLID?
Asked Answered
F

1

5

I am learning SOLID principle. While learning "Dependency Inversion Principle" found out that class should depend on interfaces rather than concrete classes.

Does this mean composition is not allowed? Also, does aggregation same as DIP?

Composition:

    class HPDesktop {
        private BluetoothMouse bluetoothMouse;
        private BluetoothKeyboard bluetoothKeyboard;
    
        public HPDesktop(){
            bluetoothMouse = new BluetoothMouse();
            bluetoothKeyboard = new BluetoothKeyboard();
        } 
     }

DIP: (This seems to be as an aggregation)

class HPDesktop {
    private Mouse mouse;
    private Keyboard keyboard;

    public HPDesktop(Mouse mouse, Keyboard keyboard){
        this.mouse = mouse;
        this.keyboard = keyboard;
    }
}
Fascist answered 11/6, 2024 at 14:35 Comment(0)
P
6

No, because you misunderstand what kinds of dependencies are being discussed.

Dependency Inversion is the concept that high level modules should not depend on low level module details, this "exposure" of the low level module details is called "leaky abstraction"

For example, let's say I'm wrapping a logging implementation with a new facade, that permits me to "do more" with logging than I was recently doing with Log4j. If I exposed the Log4j log levels as elements in my design, then I have a dependency between my facade to Log4j log levels because my facade requires Log4j.

Now, if I created my own facade log levels, then everyone who used my facade would not need to import Log4j to use Log4j's log levels. This means that if I wanted to alter what the facade used internally, I could do so without impacting the users of my facade.

How is this done? Well, interfaces provide the means of inverting dependency direction. I directly depend (in the facade) on an interface that specifies my log levels to the Log4j log levels, and some module provides these (directly depending on the same interface.

Since Log4j is now depending on the interface but as a provider of services, instead of the facade depending on log4j as a provider of services, the "provision" of "log level services" is now inverted. This means that in the future, if I want to change the implementation, I don't have to rewrite much code, just the code that presents the provision of services with my new log level provider.

Dependency Inversion

Just to drive the point home one more time, a client of the Facade using direct dependencies would have to

import facade;
import log4j;

to provide the log4j log level that facade uses. With dependency inversion, I might have to create a second set of facade log levels, but my client's import will be

import facade;

and I can later wire in a different jar to provide the log level interface implementation, meaning my client's code doesn't have to change as I swap out back-end log level providers.

Pisistratus answered 11/6, 2024 at 14:57 Comment(11)
I'm not sure exactly how to apply this answer to the question. Is the answer here that neither of the OP examples violates the DIP? Or neither of the examples is relevant to the DIP? Is this answer saying the DIP is synonymous with import statements? Are transitive dependencies not within the DIP's purview?Headfirst
It depends heavily on where the (supposed) interfaces Mouse and Keyboard are defined, and how their implementing classes are obtained. When defined in ways that don't create a direct dependency on the wrapped / hidden implementation modules, say by defining them in the facade modules and using string based factories or builders, then DIP is being used. Remember, only use DIP for the items you plan (or suspect) you need to swap out. The stuff within modules isn't inter-module dependencies. Basically, Aggregation and DIP are orthogonal (one doesn't impact the other).Pisistratus
A really good way to do this in Java is to use the "service provider framework" where you basically use a interface, and then have a "service loader" scan for implementers of the interface in other modules. baeldung.com/java-spi provides a gentle introduction. Basically, you're using a feature of the module loader to read manifest information that will do auto-wiring for you. Spring can do this too, but in a much more complicated manner, as it was created first (and thus is a little less refined).Pisistratus
I don't want to get side-tracked, but that sounds like the Service Locator Anti-Pattern. I agree aggregation is orthogonal to the DIP. The mistake in the OP is conflating aggregation with abstraction (it could be achieved using the same concrete dependencies as the composition example). The composition example is trickier: it's difficult to see how direct instantiation could satisfy the DIP. I think the mistake there is conflating composition with direct instantiation. Can you define the DIP without resorting to an example?Headfirst
You will find that (especially in the 2010s) lots of want-to-be lecturers discovered the art of headline-grabbing. I don't buy that service locator is an anti-pattern, and the book your author wrote was for .Net which always was a step behind in exploring Java ideas. His book talks about a "wiring client" which is the "GOD" class that puts your program together, as soon as you do that, to reconfigure your program (say for testing) you need a new "GOD" class to do the new wiring, and eventually when you write one that is reconfigurable, it becomes (mostly) a Service Locator.Pisistratus
As far as I have understood, Composition creates an object within a class, making its lifecycle dependent on the class. Aggregation passes an already created object into a constructor, making its lifecycle independent. DIP provides flexibility to swap objects based on business needs. It suggests passing interfaces instead of concrete classes. This allows us to change the implementation later by creating a new class that implements the interface, requiring minimal code changes. Both Composition and Aggregation can use DIP, but it is commonly practiced with Aggregation.Fascist
His primary complaint (from his book) is that Service Locators can't inject dependencies (aren't guaranteed to do so) if you leave the dependencies off the class path. That's true, but it isn't the fault of the Service Locator, it's the fault of bad packaging. Nobody can load the class if it's not there. Sure, the compiler would create a error that isn't present with a Service Locator, but then you have to make it a direct dependency to do so, not an injected one.Pisistratus
@Fascist Composition is typically used as the other approach to Inheritance. So if I'm creating a new type (class or interface) future maintenance of that type is going to be easier with composition, because with inheritance, to change details about the base class, I will likely also change details of other classes that inherit. As one is likely to get the details wrong / need to alter the details, this kind of "undoing the sharing caused by inheritance" is far harder to fix / maintain, so the end result is people writing weird code, breaking the Liskov Substitution Principle.Pisistratus
@Fascist While Aggregation is about the association between objects. Basically they are the patterns that certain objects have with each other, when those groups of objects must work as a whole. Sometimes that group had different lifecycles, sometimes they share lifecycles, sometimes they have different owners, sometimes shared. For example, a HashTable object has HashEntries which refer to Object entries. Neither HashTable or HashEntry is really useful without the other, so they are Aggregates of the HashTable functionality.Pisistratus
@Fascist The Aggregate idea came from the choice to represent Trees as Aggregates of TreeNode objects. Basically Aggregation is a group of associated objects that must have a specific structure to work, and generally that structure is exposed, while Composition is a form of Aggregation, Composition typically focuses on not exposing the details of the object pattern, but hiding them. Thus in a Tree scenario, a TreeNode is an aggregate of the Tree, and (confusingly in some libraries) maybe is a Tree in its own right too!Pisistratus
@Headfirst Keep in mind that Mark Seemann in his own book on dependency injection says that calling "ServiceLocator" and antipattern is controversial, based on this one post by Daniel Cazzulino asp-blogs.azurewebsites.net/cazzu/… which basically says that DI is bad because you don't encode your dependency in the source code, which is exactly what DI attempts to avoid. I wouldn't say it is controversial now, but I would say that early Spring did it poorly, and the JVM is much better.Pisistratus

© 2022 - 2025 — McMap. All rights reserved.