3

According to Why should I use dependency injection?, "dependency injection" has some advantages, for example:

"Non dependency injection" version:

public class Client{
    private Logger logger;
    public Client(){
        this.logger=new Logger();
    }
}

"Dependency Injection" version:

public Interface ILogger{
}
public class Client{
    private ILogger logger;
    public Client(ILogger logger){
        this.logger=logger;
    }
}

Dependency Injection version is better because:

  1. Client doesn't need to always depend on Logger, which can use other types of ILogger (eg:PrinterLogger) instead of bind to specific implementation

  2. Easier to test : Dependency Injection version doesn't need to new a Logger object first to test Client, or we may replace Logger to other ILogger such as PrinterLogger, more likely a unit test instead of integration test

  3. Looser coupling : Removing Logger class doesn't require me to modify Client to recompile

  4. More reuseable Client : I don't need to create another Client or another constructor when I need another Logger, eg: I don't need to add :

public Client(){ 
    this.logger=new PrinterLogger(); 
} 

when I want to use PrinterLogger instead of Logger.

So on one hand, "dependency injection" has the advantages above. On the other hand, when there are some codes "pass required parameters only", for example:

"Pass required parameters only" version:

public class showInfo(String name, int age){
}

this.showInfo(student.getName(),student.getAge());

it would usually be considered as code smell and want to be refactored as "preserve whole object":

"Preserve whole object" version:

public void showInfo(Student student){
}

this.showInfo(student);

Although "preserve whole object" seems considered as better style in many cases, however, I think the opposite refactoring : change "Preserve whole object" to "pass required parameters only", seems go the similar direction of "dependency injection" and have similar advantages:

  1. 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

  2. Easier to test : Pass required parameters only doesn't need to new a Student object first to test showInfo(),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

  3. Looser coupling : Removing Student class doesn't require me to modify showInfo() to recompile

  4. More reuseable showInfo() : I don't need to create another showInfo() method when I need another object (eg: I don't need to add

public void showInfo(Teacher teacher){
} 

when I want to show information of Teacher that also contains name and age instead of Student).

Although I can apply the advantages of "dependency injection" to "pass required parameters only", I believe the fact is, "Pass required parameters only" is often considered as a code smell waiting to be refactored to "Preserve whole object", instead of some other codes with code smell that waiting to be refactored to "Pass required parameters only".

So my question is, why is "Dependency Injection" ok, but not "Pass required parameters only" even if I think they are having similar goals and going to similar directions?

Also oppositely, I think I can apply the advantages of "Preserve whole object" to say "Non dependency injection" is better:

  1. "Non dependency injection" has less parameters in the Client constructor, which fits the motivation of "Preserve whole object" that shorter parameter list is better

  2. In "Non dependency injection", adding new components doesn't need to modify the parameter list in constructors,eg:

public class Client{
    private Logger logger;
    private Timer timer;
    public Client(){
        this.logger=new Logger();
        this.timer=new Timer();
    }
}

but the "Dependency injection" version needs to add a new parameter to the constructor:

public class Client{
    private ILogger logger;
    private ITimer timer;
    public Client(ILogger logger, ITimer timer){
        this.logger=logger;
        this.timer=timer;
    }
}

which "Non dependency injection" fits the situation of "preserve whole object is better" that passing "Studnet" instead of "name" and "age" allows us adding new properties (eg: dateOfBirth) without modify the parameter list

  1. "Non dependency injection" types less words when calling constructors:
Client c1=new Client();
Client c2=new Client();

instead of

Client c1=new Client(new Logger());
Client c2=new Client(new Logger());

, as if in "preserve whole object" version that showInfo() types "student" only:

showInfo(student);

instead of typing getters at each parameter:

showInfo(student.getName(),student.getAge());

But I guess you would not say "Dependency injection" is code smell because of violating the principle of "preserve whole object", and should be refactored to "Non dependency injection" version, right? So the question again : why can "dependency injection" ignore the issues that "preserve whole object" addresses?

9
  • 4
    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.
    – Flater
    Commented Aug 15 at 23:43
  • 1
    I believe the fact is, "Pass required parameters only" is often considered as a code smell waiting to be refactored to "Preserve whole object" - I think this is the root cause of this problem - you believe this, but your believe is certainly wrong. Note the opposite would also be wrong. It makes IMHO no sense to rate "Preserve whole object" vs. "Pass required parameters only" out of the context of a real program and real requirements. None of them is "better" or more "smelly" than the other, and I have no idea why you think it is.
    – Doc Brown
    Commented Aug 16 at 15:30
  • I have a strong opinion against "Preserve whole object". I often see objects being passed around leaking abstractions all over the place. I'm so traumatized by this, that I can't even force myself to find arguments for this refactoring.
    – Basilevs
    Commented Aug 16 at 18:10
  • @Basilevs - one does not "leak abstractions", one makes "abstractions" that are leaky, there's a difference. I.e., they aren't designed and maintained with enough thought and are leaking details. Commented Aug 17 at 4:21
  • 1
    'violating the principle of "preserve whole object" - this is not a principle. It's a refactoring you can choose to do, or not. It all depends on the circumstances, and on what you're trying to achieve. Sometimes it makes sense to do it. Sometimes it makes sense to have it accept a completely different object that you can convert both Student and Teacher to (although in some languages, this conversion might not be necessary). Sometimes it makes sense to pass in a base class, a common ancestor to both types, relying on polymorphism. Sometimes it makes sense to keep the parameters as primitives. Commented Aug 17 at 4:28

4 Answers 4

3

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).

4
  • I disagree strongly with "showInfo is tackling a completely different problem than what dependency injection tackles". Both manage a degree of coupling between components.
    – Basilevs
    Commented Aug 16 at 5:51
  • "Method's current implementation details using arguments" couples to controller and not to Student model. Controller is already very tightly coupled to both, so you are really playing with words here.
    – Basilevs
    Commented Aug 16 at 5:56
  • @Basilevs: Both manage a degree of coupling, but a different kind of coupling for a different reason. Referring to my analogy, both entail one person piercing another's skin with a knife but that does not mean they are the same thing.
    – Flater
    Commented Aug 17 at 22:39
  • @Basilevs: Not sure why you're bringing up controllers. Neither the question nor my answer refer to them at any point.
    – Flater
    Commented Aug 17 at 22:42
2

TLDR

Avoid accidental dependencies. The pattern to apply does not matter. Only preserve whole object, when access to all future members of the object is expected.

Accidental dependency

Both Dependency Injection and Pass required parameters deal with accidental dependencies. Accidental dependency is a dependency that is not necessary for the implementation, but is an artifact of the type system (or, more generally, programming language) in use.

Example 1

When our component does:

this.logger=new Logger();

it depends on standard output because default logger outputs on standard output. But our component is not interested in that detail. It just wants to log somewhere. Such a dependency is accidental (unnecessary, avoidable, has nothing to do with domain). If our language would allow overriding behavior of Logger (using Aspect Oriented Programming, for example), then our implementation would no longer depend on standard output and would not have this dependency. This dependency is caused by limitations of our language and type system which makes it accidental.

Example 2

public void showInfo(Student student){
    student.getName();
}

In this example, our component implicitly depends on student.getAge() method. It would be possible to avoid the dependency in a relaxed dynamic type system, so again, the dependency arises from language specification detail, hence is accidental.

Note, that in both examples, not only we introduce dependencies to showInfo() method, but we also introduce dependencies from "showLibrary" to "PersonnelLibrary" and "ioLibrary". In other words, class dependency causes package, library, module etc. dependencies. All of those are accidental!

The danger of accidental dependencies

Accidental dependencies seem harmless - they do not affect runtime of our program in the slightest. Why people try to avoid them then? To explain this, we need to pay attention to project's (codebase's) history.

  1. Accidental dependencies can become real dependencies easily and stealthily - a single change to a method burdened with an accidental dependency would be completely unnoticed by compiler, code reviewer and even author.
  2. Therefore accidental dependencies are indistinguishable from real ones when performing a high-level review.
  3. Therefore later changes would have to treat all dependencies equally.
  4. As dependencies are hard to remove without breaking clients, dependencies will have to persist.
  5. Therefore adding dependency is mostly irreversible.
  6. Therefore adding accidental dependency complicates dependency graph forever without adding useful functionality.
  7. Accelerated speed of dependency graph growth makes maintenance harder.

This is the reason why package repositories (NPM, PyPi, Maven, etc) have so many small and seemingly stupid packages - to include a large dependency in a project is an enormous risk, because it always adds a lot of accidental dependencies, so people try to use smallest component possible or none at all to avoid those.

Why preserve whole object?

It may seem that passing an object argument with complex interface would leak a lot of dependencies into a method. Would not those be accidental? To answer method's author has to decide what is needed for its implementation. Or to paraphrase, what additions to the argument's interface are more likely - new necessary dependencies or new accidental dependencies?

Example 3

public void showInfo(Student student){
    student.getName();
    student.getAge();
    # student.getCourses(); // Does not exist yet. Do we want to show courses?
} 

If next iteration of Student class is likely to add getCourses() method, but showInfo() is not interested in those, the new dependency would be accidental (and bad). If showInfo() is used for debugging and shows absolutely all fields of argument, than the dependency would be required (and good).

This example demonstrates:

  • There are scenarios where new dependencies are not accidental
  • Such scenarios are rare (in my experience, a requirement to have all fields of an object available is always a design flaw, and debugging purpose is a very poor excuse)

Potential solutions

In the original question, author tries to marry two components - Student and showInfo(). Tight coupling makes showInfo() flexible at cost of rigidity for Student and accidental dependencies. Splitting Student into multiple arguments loosens the coupling, but flexibility is not achieved. How can we loosen the coupling while preserving flexibility? With indirection. Introducing Adapter pattern:

interface StudentInfo {
   String getName();
   int getAge();
}
public void showInfo(StudentInfo info);

showInfo(adapt(student));

As long as adapt() is implemented in a component that already depends on both Student and showInfo() no new dependency is (or will ever be) introduced with this approach. Adapter approach is very similar to argument splitting, but isolates actual dependencies in a single adapt() method which can be reused in multiple contexts.

Note, adapter pattern can "solve" all dependency problems. This does not mean you have to use it. Additional indirection slightly degrades readability, performance, memory pressure. Use it with care.

Conclusion

The API design is always a balancing act between flexibility and maintainability. Author has to take into account the intent of introducing dependency and project future potential needs of both components. From that they have to derive both useful and accidental dependencies and avoid accidental ones.

2
  • This looks all correct to me, still I have some trouble to map this to what the OP asked, at least what they asked literally.
    – Doc Brown
    Commented Aug 16 at 15:41
  • @DocBrown They ask - "Why Preserve whole object?" I say - "no real/strong reason", and explain why.
    – Basilevs
    Commented Aug 16 at 16:27
0

Neither this question nor the linked question provides any justification about why passing 'value' parameters might be a 'code smell' so it's impossible to address that particular claim without doing some wild guesswork about context. However in general there is absolutely nothing wrong with methods that accept plain simple primitive values/data as parameters.

Overall the two scenarios being compared are completely different:

  • The first scenario describes object creation and composition using DI (which as @Basilev points out is a form of parameter passing)
  • The second scenario describes calling a method (which may be a method on an an existing object, or perhaps a static method/function)

Dependency injection is generally addressing the creation of objects through composition - the term 'dependency' used to imply the objective being related to code structure.

The example in the question of injecting a logger is a very common use of Dependency injection because loggers represent cross-cutting concerns, and logger objects themselves are probably going to be long-lived for most of the program's lifetime (sometimes known as 'singleton'). Loggers tend to be used in a lot of places, so passing a logger instance from the root of the program into every method would probably make the code feel rather bloated and cumbersome.

On the other hand, calling a method and passing a parameter is just that - there's nothing deeper behind it - such methods usually have a parameter list that only makes sense for that particular method, rather than being some cross-cutting list of parameters.

There's nothing inherently wrong with methods which accept individual values as arguments, though of course it could become cumbersome if the program starts to manifest a lot of different methods and behaviours which each use long lists of exactly the same parameters. But there's no hard-fast rule anywhere.

Some static code analysis tools may be configured to detect long parameter lists and flag this as a possible indicator of a problem (Usually a configurable threshold), but such warnings could equally apply to constructors accepting too many dependencies as well.

4
  • Dependencies are injected via argument passing (constructor or otherwise), so I'm having difficulties parsing this answer. The graph of dependencies does not have to be based only on objects, methods could be nodes too.
    – Basilevs
    Commented Aug 15 at 8:17
  • @Basilevs That's correct, but I'm not sure how that relates to the wording of this answer? I'm specifically referring to the example in the question and avoiding going into a general description of Dependency Injection Commented Aug 15 at 8:29
  • 1
    You make parameters passing and dependency injection oppose each other. But they are almost the same in my book. Indeed DI is parameter passing with restrictions.
    – Basilevs
    Commented Aug 15 at 8:31
  • @Basilevs Ah I see, no that's not the intention at all. I agree that DI is parameter passing - my intention is to compare object creation/initialisation/composition with calling a method on an already-existing object, I will try to clarify that in the answer, thanks. Commented Aug 15 at 8:33
0

You did not specify where showInfo is located. In OOP fashion, it should probably be implemented on the Student class directly.

However, it seems to be independent of the student in your case, so your point about decoupling student and showInfo is a good Idea. However, separating the arguments is not the way to go, as they belong together. Instead, the component which owns showInfo could provide the interface (or trait or...) NameAgeInfo containing the required accessors: showInfo(NameAgeInfo Info). Decoupled and implementable by your Students or Teachers while still providing a meaningful abstraction.

You can further decouple it by providing an Adapter if your student should be unaware of NameAgeInfo, introducing a third component that depends on both.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.