This is a long answer. I couldn't really avoid it. You are bringing up several different software engineering principles, and have started oversimplifying, overcomplicating, and conflating them into a tangled ball of what I'm going to call a mistaken interpretation of what the guidelines actually tell you. This answer attempts to untangle this ball back into the actual underlying principles that you should be adhering to (without tangling it all up again).
The student example
showInfo(Student student)
is a method whose job it is to display student information (regardless of which of the student's fields will be displayed).
showInfo(String name, int age)
is a method whose job it is to display a name and age (regardless of where these values were sourced from).
These are two completely different responsibilities and they cannot be judged on syntax alone, like you're trying to do in this question. You need to consider the purpose, use cases and intended lifecycle of the method. All of that is missing from your question, making that part unanswerable.
The short answer here is that the design of showInfo
is tackling a completely different problem than what dependency injection tackles. You're not seeing the wood for the trees here, by which I mean that you're trying to judge things by their syntax alone instead of considering the bigger solution that they're a part of.
Your main question is on the concept of dependencies, not on this specific example, so I'm skipping the long answer on this example.
Injected dependencies and method parameters
"Dependency" is a dangerously ambiguous word to use here, which is why I'm very intentionally referring to injected dependencies when dealing with dependencies that are method parameters; and not just referring to the abstract concept of what a dependency is. I will refer to the latter as "tight coupling" to make the distinction clear.
When you consider the syntax alone, injected dependencies and method parameters are arguably one and the same. The distinction between which method parameter is a "value" and which is an "injected dependency" is irrelevant to the compiler, in either case it's a parameter you pass into a method (whether that's an object's constructor or one of its methods).
But dependencies live on a higher level than syntax alone. The above observation is correct but it is not helpful in any way.
public class Client{
private Logger logger;
public Client(){
this.logger=new Logger();
}
}
What you have here is tight coupling between the Client
and Logger
classes. This manifests in both directions: the overall application is not able to choose which Logger
a Client
uses, and Client
will break when this specific Logger
is deprecated.
Think of it like a coffee shop that buys the cups in which they sell their coffee. This presents a tight coupling in two ways: customers cannot choose which cup they get their coffee in, and when those cups are no longer sold, the coffee shop can't sell more coffee until they find a new cup supplier.
Dependency injection is like allowing customers to bring in their own cup. This breaks the tight coupling in both directions: the customer gets to have their coffee in whatever cup they want (within the constraints set by the coffee shop), and the coffee shop no longer has to bother with setting up a supply chain with a specific cup manufacturer.
Applying "bring your own cup" to the technical example, it becomes a case of "provide your own Logger
", which is exactly this:
public class Client{
private Logger logger;
public Client(Logger logger){
this.logger=logger;
}
}
I have removed the interface from the example purely to boil it down to the core of what dependency injection is. At the end of the day, I strongly advocate that you use interfaces as well here, for related reasons of code cleanliness.
That's the solution. Based on the posed question, you already understand the solution, but you are finding alleged conflicts when applying this solution to other topics. I'll address your specific concerns in the next section, but the TL;DR is that you're wrongly applying a (by itself correct) observation about dependency injection to a completely different problem (i.e. that of method parameter design), and that just doesn't apply there.
Direct feedback to some things you said
showInfo() doesn't need to depend on Student, which can use other types of objects that also contain name and age (eg: Teacher) instead of binding specific implementation
Mostly addressed in the first section; the choice of what method parameters to use is largely unrelated to the method body implementation, and is more related to the method signature (which implies the design and therefore intended purpose of the method).
If showInfo
's purpose is to display student information, then you should design that method signature based on that intention (i.e. "here's a Student
, please display it") instead of basing in on the method's current implementation details ("I know for a fact that you only display a student's name and age, so I'm only going to give you those").
The latter is a form of tight coupling (contrary to your interpretation), because it requires the method signature to be aware of the method implementation, and any change made to the implementation (e.g. we want to display their birthdate instead) now forces you to also update your method signature.
If you had been passing a Student
parameter, you would not have needed to update the method signature just because the method implementation decided to use different Student
fields to display.
Easer to test : Pass required parameters only doesn't need to new a Student object first to test showInfo()
"Easier to test" should not be a measure of needing to type less. "Easier to test" should be a measure of how closely it matches the real world usage without requiring elaborate setup.
In the scenario where this method's purpose is to display Student
information, it's obvious that your test is going to be handling a Student
object as part of its execution.
Trying to focus on only needing to provide the string name and integer age is a variation on primitive obsession (from a different angle than is usually the case).
or we may replace Student with other objects that also has name and age such as Teacher at the calling client side, more likely a unit test instead of integration test
This is a nonsensical observation from the perspective of a test, since a test should strive to reflect real usage as best as it can. If you start using different objects in your test than in your real code, for no discernible reason than wanting to be able to do so, taking it so far as to change how you design your code in order to be able to achieve this; I'm going to question your overall testing methodology.
Looser coupling : Removing Student class doesn't require me to modify showInfo() to recompile
Nope, it's tighter coupling, because any change to the method implementation now also blows back on the method signature, and therefore every consumer of that method.
With a Student
parameter, on the assumption that this method's purpose was to display student information, a change to the method implementation would not have blown back on the method signature (nor any consumer of the method)
Tight coupling is usually best measured as a form of "blast radius" when the code has to change in the future. The bigger the blast radius, the tighter the coupling.
More reuseable showInfo() : I don't need to create another showInfo() method when I need another object (eg: I don't need to add
This isn't wrong, but like I mentioned in the first section, it's describing a different method with a different purpose. Yeah, if you're designing a method that centers around those specific values and not the source of those values, then using those values directly (instead of the irrelevant wrapper class that they are located in) in the method signature is the better choice.
Interestingly, when this is the method's purpose, using the string/int params is looser coupling than using a Student
param.
I just want to point out here that given the exact same syntax difference (i.e. either a Student
or string+int
signature), the decision on which of these is the loosest coupling hinges on the purpose of the method, not it's implementation.
This is why I've been saying that you cannot judge this based on syntax alone, you need to understand the larger design principle at play.
"Non dependency injection" has less parameters in the Client constructor, which fits the motivation of "Preserve whole object" that shorter parameter list is better
Apples and oranges.
You're conflating very different things here. Adding a new and completely unrelated parameter (to an existing set of parameters) for its own reasons is a very different scenario to having multiple parameters that together express a single coherent concept.
In "Non dependency injection", adding new components doesn't need to modify the parameter list in constructors [..] but the "Dependency injection" version needs to add a new parameter to the constructor
You're measuring the wrong thing.
What you're implying here is that you don't want to update any usage of a constructor, and therefore you will steer your design in a way that avoids doing so. This is inverted thinking.
Implementing injected dependencies via the constructor inherently means that when the list of dependencies changes, you'll be changing the constructor signature to match that. Updating the constructor's signature is not a bad situation that you should try to avoid at all costs, it the whole point of injecting your dependencies in the first place.
It seems like you've selectively forgotten all the reasons for doing dependency injection (which you listed in your question and then said you agreed with), and throwing it all out on the notion that it might require you to update a constructor method signature.
Being worried about a changing constructor signature is already pointless on its own; but it becomes egregiously pointless when you consider that a vast majority of dependency injection uses containers that dynamically adjust to a constructor signature and automatically source and inject all requested dependencies anyway.
"Non dependency injection" types less words
If word count is your measure of success, you should write newspaper headlines for a living, and you should not be let anywhere near code design.
The productivity of a developer is not measured in word or line count. Any assertion to that end is plainly asinine, generally finding its roots in metrics designed by non-technical management that doesn't trust their technical staff's input on what actual productivity looks like. I have no good words for anyone who tries to vouch for character count as a meaningful measure of anything when discussing software engineering.
So the question again : why can "dependency injection" ignore the issues that "preserve whole object" addresses?
I know I've already mentioned this several times, but the issue with your question is that you're comparing apples and oranges. It's the equivalent of asking
Bob went to jail because he stabbed someone with a knife. But Bill used a scalpel when operating on their patient. A scalpel is a knife. Why can Bill stay out of jail when Bob can't?
It may seem like a very easy to spot mistake in the above analogy, but your question is similarly comparing two very different contexts (assault vs surgery; dependency inversion vs method responsibilities) based solely on the implementation details (both involve cutting someone with a knife; both use a particular method parameter type).
showInfo(Student student)
is a method whose job it is to display student information (regardless of which of the student's fields will be displayed).showInfo(String name, int age)
is a method whose job it is to display a name and age (regardless of where these values were sourced from). These are two completely different responsibilities and they cannot be judged on syntax alone, like you're trying to do in this question. You need to consider the purpose, use cases and intended lifecycle of the method. All of that is missing from your question, making that part unanswerable.