3

Where should I inject the dependency when I write a class? Should it be given to __init__ or to the specific method that uses the dependent object?

Take the below two pieces of code for example, to me they function exactly the same way:

class TextFile:
    def save_text(self, text):
        print("Below texts saved to file")
        print(text)

class TextWriter:
    def __init__(self):
        self.text = ""
        
    def write_text(self, text):
        self.text += text
        
    def save_text(self, saver): # dependency injected in the specific method
        saver.save_text(self.text) 

if __name__ == "__main__":
    tf_saver = TextFile()
    writer = TextWriter()
    writer.write_text("Here is some text")
    writer.save_text(saver=tf_saver)
class TextFile:
    def save_text(self, text):
        print("Below texts saved to file")
        print(text)

class TextWriter:
    def __init__(self, saver): # dependency injected in the constructor
        self.text = ""
        self.saver = saver
        
    def write_text(self, text):
        self.text += text
        
    def save_text(self):
        self.saver.save_text(self.text) 

if __name__ == "__main__":
    tf_saver = TextFile()
    writer = TextWriter(tf_saver)
    writer.write_text("Here is some text")
    writer.save_text()

Which design is better in this case, and why? In general, what is the rule to decide if the dependency should be given to the constructor or the specific method?

2
  • 1
    method injection...as if we already had less of estoric terms coined for every quirk of code structure. Anyhow, in simplest terms: The dependencies that stay same for lifetime of an object should ideally be passed via constructor, while the dependencies that are local per method usage, should be provided as method parameters, together.
    – S.D.
    Commented Apr 20, 2023 at 8:14
  • 1
    @candied_orange said, "It's all about the using code" I.e., all about the client of your TextWriter class. Suppose I'm building a big system that uses a TextWriter. You're offering two choices; Either (a) the code site that constructs the TextWriter is responsible to decide where/how the text should be saved, or (b) the site that knows when to save must accept that responsibility. Those two sites could be far from each other in an architecture diagram, It's two different ways of thinking about what TextWriter does. IMO (a) feels like it should be called DI, but (b) does not. Commented Apr 20, 2023 at 14:40

4 Answers 4

4

There's really no one correct answer here and I think both are completely valid. The constructor injection is more grounded in OO approaches and the function injection is more aligned with functional-style programming.

The primary way I would determine which is more appropriate would be based how aligned the lifecycle of the object is with its association to the saver object. If an instance of TextWriter will only ever need to use one saver then I'd probably assign it in the constructor, as long as it's available at the time of construction. On the other hand, if the saver is potentially specific to each call of the method, it makes more sense to me to pass it into the method instead of creating multiple objects for no other reason.

That's all pretty straightforward and relatively language agnostic. There are a few other more nuances around this kind of thing. One is that nothing is stopping you from doing both e.g.:

class TextWriter:
    def __init__(self, saver): # dependency injected in the constructor
        self.text = ""
        self.saver = saver
        
    def write_text(self, text):
        self.text += text
        
    def save_text(self, saver=None):
        if saver is None:
           saver = self.saver

        saver.save_text(self.text)

I believe the above is pretty idiomatic Python. This approach can be adapted more-or-less to work in any OO language. In Java for example, you could use overloaded methods to achieve a similar result. In a nutshell, if you pass in a saver, it's used, if you don't, the constructor injected saver is used.

As candied_orange points out, if the site where the save method is called doesn't know about savers (e.g.: a 3rd-party library) then using the method injection approach would seem to be problematic. There are functional approaches to address that, however. The functools package has a lot of useful helper functions and one of those is partial. The partial function takes a method and some parameters and returns a wrapper function that implicitly applies them. Or, if you prefer you can do something similar in code as described in the beginning the accepted answer to this question.

The upshot is that I don't think you should worry about which one is better in general but consider how the objects and functions need to be used. I'd go so far as to say that you should almost always be thinking about class design this way. I often find I end up with better class design if I write code that will use a class and its objects before I define the class.

1
  • 3
    "I often find I end up with better class design if I write code that will use a class and its objects before I define the class." +100
    – svidgen
    Commented Apr 19, 2023 at 21:42
6

It's all about the using code.

Constructor injection, done like this:

tf_saver = TextFile()
writer = TextWriter(tf_saver)
writer.write_text("Here is some text")
writer.save_text()

has no advantage over method injection:

tf_saver = TextFile()
writer = TextWriter()
writer.write_text("Here is some text")
writer.save_text(saver=tf_saver)

The advantage comes when the using code takes advantage of the ability to decouple knowing what and when:

def timeToWrite(Writer writer):
    writer.write_text("Here is some text")
    writer.save_text()

Here, timeToWrite has no idea what it's writing to. It just knows when to write (when it's called). Whatever built writer figures out what to write to. That decoupling enables separation of concerns. One of the most profound design questions is: what knows about what? The ability to limit the spread of knowledge is a powerful tool.

5
  • 4
    "Constructor injection ... has no advantage over method injection" this is incorrect - it has the two advantages I listed in my answer: It forces variable initialization and potential detects cyclic dependencies (as you are unable to sequence the constructor calls if a cyclic dependency exists).
    – DavidT
    Commented Apr 19, 2023 at 8:04
  • FWIW, you could also say method injection has the advantage of allowing cyclical dependencies, if you're into that sort of thing!
    – svidgen
    Commented Apr 19, 2023 at 21:39
  • 1
    I do not encourage the method injection example provided here, especially when writer is shared.
    – S.D.
    Commented Apr 20, 2023 at 8:03
  • @S.D. care to elaborate? Commented Apr 20, 2023 at 17:21
  • 1
    to @DavidT comment - constructor injection also addresses the general Inversion of Control (IOC) abstraction.
    – radarbob
    Commented Apr 20, 2023 at 23:02
0

As long as you understand why you're favoring one flavor over the other, whatever you choose is probably correct. As @JimmyJames's answer states:

... I end up with better class design if I write code that will use a class and its objects before I define the class.

When do you do this, I think you'll find other well-known principles comes into play — DRY in particular.

Let's start with method injection. Does your code look like this?

# do thing A
writer.write_text("did thing a", destination);

# do thing B
writer.write_text("did thing b", destination);

# do thing C
writer.write_text("did thing c", destination);

Or this?

# do thing A
writer.write_text("did thing a", destinationA);

# do thing B
writer.write_text("did thing b", destinationB);

# do thing C
writer.write_text("did thing c", destinationC);

If it's the former, your code is "WET", and you can DRY it out by injecting destination into the constructor. If it's the latter, your code is already DRY, and you'd only make it WET by favoring constructor injection:

# do thing A
writer = TextWriter(destinationA)
writer.write_text("did thing a");

# do thing B
writer = TextWriter(destinationB)
writer.write_text("did thing b");

# do thing C
writer = TextWriter(destinationC)
writer.write_text("did thing c");

Gross.


As an aside, my opinion has been generalized. But, in your particular example, a "writer" object like the one you've designed would make ZERO no sense to me using "method injection". Method injection gives me no intuitive clue that writer.write_text('ABC', destination) will dig up all my past write activity and write all of it to destinationB. Makes no sense to me whatsoever, TBH.

If this actually represents a use-case you're implementing, it smells like a logger. Just use a logger. Or build something that models a more traditional logger with log levels and teed outputs if you need them.

0

Kotlin

If I may, I would like to start with a different language (Kotlin) because the syntactic sugar makes the example cleaner. In Kotlin constructor injection would look like this:

class MyClass(private val myDep: MyDep) {
    ...
}
  • The "val" causes the compiler to create a class variable (this.myDep) automatically and make it immutable so it can't be re-assigned.
  • The "private" prevents other classes from accessing the variable.

In Kotlin I would prefer constructor injection for 3 reasons:

  1. The syntax is concise.
  2. Variables initialized by the constructor will always be available - it's not possible to forget a variable when constructing an object.
  3. The variable is immutable - I know it's not going to change after construction (which is why immutability is often recommended in Kotlin).

Cyclic Dependencies

Typically the only time I would not use constructor injection is when there is a cyclic dependency i.e:

class MyA(private val b: MyB) {
    ...
}

class MyB(private val a: MyA) {
    ...
}

There is a catch 22 here:

  • I can't created a MyA without first creating a MyB
  • I can't created a MyB without first creating a MyA

There are some technical work arounds for this problem (Dynamic or Delegating Proxies) but I would generally change one class to use a property to set the dependency, so that the logic becomes:

val a = MyA()
val b = MyB(a)
a.b = b

Edit:

I view cyclic dependencies as bad and should be avoided when possible, hence I do see constructor injection as preferable over method injection.

Allowing the use of cyclic dependencies is "necessary" in so much as there are times when you have to ship product and don't have time to refactor the code to remove the cyclic dependency - under these circumstances I "understand" the need to use method injection to support the cyclic dependency.

Python

In python two of the previous points still hold:

  • As long as you sanity check your "init" parameters, you can typically assume that the variable has been assigned for the rest of your code - if you use a method, it is possible that the calling code never calls the "setter" method.
  • Init/Constructor injection may not work if you have cyclic dependencies.

Python doesn't have a way to mark instance variables immutable, so you don't get that advantage in Python and it's arguable if there is any syntactic advantage to "init" vs method based injection in Python.

Hack

In both Kotlin and Python you can also resolve cyclic dependencies by chaining constructors, i.e.:

class MyA:
    def __init__(self):
        self._b = MyB(self)
        // More initialization code here..

class MyB:
    def __init__(self, a):
        self._a = a

This is considered a hack because MyA is not fully constructed at the moment it starts construction of MyB.

3
  • your cyclic dependency would still occur with method injection, you just wouldn't get a compile time error
    – Ewan
    Commented Apr 19, 2023 at 19:00
  • @Ewan Huh? I thought DavidT was arguing that an advantage of constructors was that they don't let you set up cyclic dependencies the way method injection does. Commented Apr 19, 2023 at 19:15
  • 1
    @Ewan to be clear, I do think constructors not being able to create cyclic dependencies is an advantage. Cyclic dependencies1,2 are a special kind of hell. It's just not the lesson the OP needed with this code. Commented Apr 19, 2023 at 21:24

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.