1

I have class A that depends on interfaces B & C. I use constructor injection to inject them into A:

class A {
    let b: B
    let c: C
    
    init(b: B, c: C) {
        self.b = b
        self.c = c
    }
}
    

This is mostly so I can inject mock objects for testing.

However, 80% of my use cases use the default implementations of B & C (let's call them, BImpl and CImpl). There's also some edge cases that make up 20% of my use cases.

Is it a good idea to create a "default" initializer where the constructor is parameterless and sensible defaults are created?

init() {
    self.init(b: BImpl(), c: CImpl())
}

The initializer delegates to the previous (parameterized) initializer, so I don't lose my ability to inject my own (custom) dependencies if I wanted to. But this way, I can also choose to roll with the default values if I want to.

Does this have some unforeseen disadvantages?

5
  • 2
    How do you propose to unit test any code which uses your default initializer? Jul 6 '20 at 8:13
  • @PhilipKendall Since the default initializer just passes in the implementations of B & C to the parameterized initializer, it's the same as injecting BImpl and CImpl into the parameterized constructor. If I wanted to use BImpl2 and CImpl2, I would use the parameterized constructor, not the default.
    – RodYt
    Jul 6 '20 at 8:50
  • 1
    I know that :-) The point is that as soon as you use your proposed default constructor, you can no longer unit test the code which uses it, because you've removed your ability to mock out B and C, so have to test that functionality as well. Jul 6 '20 at 9:07
  • @PhilipKendall - I get what you're saying, and it's certainly something to be aware of, but that's only a problem if the code in question initializes the A instance by itself (i.e., internally), using the default ctor; if it takes an instance of A as a dependency, than it can be isolated in a test. It's not really an issue if the default ctor is called at the composition root. However, it does feel like it's more prone to produce the scenario that you're describing (e.g., in a team setting, where not all devs working on the code are aware of how the dependency structure should look like). Jul 6 '20 at 19:26
  • That said, if you are going to use a container to manage dependencies, you might as well configure the parameters there, rather than have a default constructor. If you are doing ad hoc DI (manual DI, no container), it may make more sense. Jul 6 '20 at 19:29
2

One of the benefits of dependency injections is that you can compile, test, and use A without "knowledge" of BImpl or CImpl, but by adding them as defaults, you lose that benefit. You can still use dependency injection in your edge-cases (though Philip makes a good case about transitive dependencies), but you will always be paying the price for having the defaults, used or not.

If your project is small enough that it's all in one module and build times don't bother you, it's probably not a "real" problem. If you are working on something big or writing library code, you should pay careful attention to which modules know about each other, and which direction that knowledge goes.

You might want to use a "dependency injection container" to manage these default dependencies, rather than coding it into all of your classes. It can even be more convenient.

2
  • So what you're saying is that if we decide to say, remove BImpl and CImpl, we would have to modify A as well, since our default constructor is coupled to it (v.s. DI-only). (However, this isn't major enough to create problems on a massive scale.) Do you have any proposed solution that doesn't use an IOC container?
    – RodYt
    Jul 6 '20 at 9:41
  • @RodYt you can use a factory or builder to set up A as Pedro suggests, or simply provide the instances of B and C directly in the code that creates an A, like I assume you are doing in the special cases.. Jul 6 '20 at 12:37
0

Should A, B, C, know you have an application D where B and C are always BImpl and CImpl? I don't think so. That is a concern of D and no one else.

What need is a factory method.

Have a method that encapsulates that default logic for the D application, and leave that out of the actual interfaces and implementations.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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