2

I have been using dependency injection using @Autowired in Spring boot. From all the articles that I have read about dependency injection, they mention that dependency injection is very useful when we (if) decide to change the implementing class in the future.

For example, let us deal with a Car class and a Wheel interface. The Car class requires an implementation of the Wheel interface for it to work. So, we go ahead and use dependency injection in this scenario

// Wheel interface
public interface Wheel{
   public int wheelCount();
   public void wheelName();
...
}


// Wheel interface implementation
public class MRF impements Wheel{
   @Override
   public int wheelCount(){
   ......
}...
}



// Car class
public class Car {

    @Autowired
    Wheel wheel;
}


Now in the above scenario, ApplicationContext will figure out that there is an implementation of the Wheel interface and thus bind it to the Car class. In the future, if we change the implementation to say, XYZWheel implementing class and remove the MRF implementation, then the same should work.

However, if we decide to keep both the implementations of Wheel interface in our application, then we will need to specifically mention the dependency we are interested in while Autowiring it. So, the changes would be as follows -

// Wheel interface
public interface Wheel{
   public int wheelCount();
   public void wheelName();
...
}

@Qualifier("MRF")
// Wheel interface implementation
public class MRF impements Wheel{
   @Override
   public int wheelCount(){
   ......
}...
}

// Wheel interface implementation
@Qualifier("XYZWheel")
public class XYZWheel impements Wheel{
   @Override
   public int wheelCount(){
   ......
}...
}


// Car class
public class Car {

    @Autowired
    @Qualifier("XYZWheel")
    Wheel wheel;
}


So, now I have to manually define the specific implementation that I want to Autowire. So, how does dependency injection help here ? I can very well use the new operator to actually instantiate the implementing class that I need instead of relying on Spring to autowire it for me.

So my question is, what are the benefit of autowiring/dependency injection when I have multiple implementing classes and thus I need to manually specify the type I am interested in ?

Boudhayan Dev
  • 970
  • 4
  • 14
  • 42
  • 1
    You create boundaries. `Car` does not care for the change. You can implement it in a way so that you do not have to change anything in `Car` (using `@Primary` in `XYZWheel`). Should, for any chance, something be broken in the new `XYZWheel`, the bug is contained within the unit of `XYZWheel`. Some remarks: you should use `@Inject` instead of `@Autowired` --- You should prefer constructor injection over field injection. – Turing85 Dec 27 '19 at 09:24
  • 1
    Checkout this question: https://stackoverflow.com/questions/34350865/spring-choose-bean-implementation-at-runtime – Simon Martinelli Dec 27 '19 at 09:25
  • 1
    @Turing85 using `@Primary` just moves the concern to the implementing class , doesn't it ? Instead of using a Qualifier,we now specify our bean of choice in the implementing class. Also, if something is broken in `XYZWheel`, how does autowiring it solve the issue ? I'll eventually run in to the error if I am using the XYZ bean whether I use Autowired or manually instantiate its bean using `new` – Boudhayan Dev Dec 27 '19 at 09:27
  • @SimonMartinelli Thanks but that link talks about choosing bean implementation at run time, which I am aware of. I am more interested in knowing what **benefit** does DI provide when I have to specifically mention the bean i am interested in, when multiple implementing class exists ? – Boudhayan Dev Dec 27 '19 at 09:29
  • It does move the concern to the implementation, where it belongs. This is basically the whole point in IoC (and, to that extend, dependency injection): move the condern out of the root class. If the architecture is cleanly designed, `Wheel` is not exposed to other classes. Thus, other classes cannot be influenced by a bug in `XYZWheel`. – Turing85 Dec 27 '19 at 09:30
  • 1
    That's exactly the use case. If you have multiple implementation you usually want to choose at runtime the implementation. If you can choose at compile time you don't even need the interface. – Simon Martinelli Dec 27 '19 at 09:34
  • 1
    More precisely: you do not want to actively choose. In most cases, the decision is done through presenece (the first scenario OP described): The database driver is choosen by putting one (and only one!) database driver on the class path. If there is none or are multiple, we will most probably see an error from the DI container. – Turing85 Dec 27 '19 at 09:37
  • Okay, @SimonMartinelli I gave it a read one more time. Makes sense. – Boudhayan Dev Dec 27 '19 at 09:43
  • @SimonMartinelli: * If you can choose at compile time you don't even need the interface* - that is an incorrect statement. If you have on different places in the code several beans of the same type but initialized differently, you use a factory that initializes these beans for you and you just use different qualifiers to distinguish them. One class is sufficient. According to your statement for each case you would introduce a new class. This would be very inefficient, you would increase maintenance costs. – mentallurg Dec 27 '19 at 10:20

2 Answers2

2

You don't have to necessarily hard-wire an implementation if you selectively use the qualifier for @Primary and @Conditional for setting up your beans.

A real-world example for this applies to implementation of authentication. For our application, we have a real auth service that integrates to another system, and a mocked one for when we want to do local testing without depending on that system.

This is the base user details service for auth. We do not specify any qualifiers for it, even though there are potentially two @Service targets for it, Mock and Real.

@Autowired
BaseUserDetailsService userDetailsService;

This base service is abstract and has all the implementations of methods that are shared between mock and real auth, and two methods related specifically to mock that throw exceptions by default, so our Real auth service can't accidentally be used to mock.

public abstract class BaseUserDetailsService implements UserDetailsService {
    public void mockUser(AuthorizedUserPrincipal authorizedUserPrincipal) {
        throw new AuthException("Default service cannot mock users!");
    }

    public UserDetails getMockedUser() {
        throw new AuthException("Default service cannot fetch mock users!");
    }

    //... other methods related to user details
}

From there, we have the real auth service extending this base class, and being @Primary.

@Service
@Primary
@ConditionalOnProperty(
        value="app.mockAuthenticationEnabled",
        havingValue = "false",
        matchIfMissing = true)
public class RealUserDetailsService extends BaseUserDetailsService {

}

This class may seem sparse, because it is. The base service this implements was originally the only authentication service at one point, and we extended it to support mock auth, and have an extended class become the "real" auth. Real auth is the primary auth and is always enabled unless mock auth is enabled.


We also have the mocked auth service, which has a few overrides to actually mock, and a warning:

@Slf4j
@Service
@ConditionalOnProperty(value = "app.mockAuthenticationEnabled")
public class MockUserDetailsService extends BaseUserDetailsService {

    private User mockedUser;

    @PostConstruct
    public void sendMessage() {
        log.warn("!!! Mock user authentication is enabled !!!");
    }

    @Override
    public void mockUser(AuthorizedUserPrincipal authorizedUserPrincipal) {
        log.warn("Mocked user is being created: " + authorizedUserPrincipal.toString());
        user = authorizedUserPrincipal;
    }

    @Override
    public UserDetails getMockedUser() {
        log.warn("Mocked user is being fetched from the system! ");
        return mockedUser;
    }
}

We use these methods in an endpoint dedicated to mocking, which is also conditional:

@RestController
@RequestMapping("/api/mockUser")
@ConditionalOnProperty(value = "app.mockAuthenticationEnabled")
public class MockAuthController {
    //...
}

In our application settings, we can toggle mock auth with a simple property.

app:
  mockAuthenticationEnabled: true

With the conditional properties, we should never have more than one auth service ready, but even if we do, we don't have any conflicts.

  1. Something went horribly wrong: no Real, no Mock - Application fails to start, no bean.
  2. mockAuthEnabled = true: no Real, Mock - Application uses Mock.
  3. mockAuthEnabled = false: Real, no Mock - Application uses Real.
  4. Something went horribly wrong: Real AND Mock both - Application uses Real bean.
Compass
  • 5,867
  • 4
  • 30
  • 42
  • Thanks for the explanation. I wasn't aware of autowiring extended class with reference of the Base class. You have used `@Autowired BaseUserDetailsService userDetailsService` and then you have 2 class extending the BaseUserDetailsService. Conditionally either of the 2 extended classes/beans are injected into the above autowired instance. So, this behavior is exactly same if we were to autowire a Interface (instead of a class) and then conditionally provide beans of the implementing classes. Is my understanding correct ? – Boudhayan Dev Dec 30 '19 at 03:32
  • Yes, actually it should be an abstract class, just haven't gotten around to refactoring it XD. But you could use the class as long as you don't use `@Service` annotation on the base class. – Compass Dec 30 '19 at 15:13
1

The best way (I think) to understand Dependency Injection (DI) is like this :

DI is a mecanism that allows you to dynamically replace your @autowired interface by your implementation at run time. This is the role of your DI framework (Spring, Guice etc...) to perform this action.

In your Car example, you create an instance of your Wheel as an interface, but during the execution, Spring creates an instance of your implementation such as MRF or XYZWheel. To answer your question:

I think it depends on the logic you want to implement. This is not the role of your DI framework to choose which kind of Wheel you want for your Car. Somehow you will have to define the interfaces you want to inject as dependencies.

Please any other answer will be useful, because DI is sometimes source of confusion. Thanks in advance.

Harry Coder
  • 2,429
  • 2
  • 28
  • 32