Boudhayan Dev
Boudhayan Dev

Reputation: 1029

How is dependency injection helpful when we have multiple implementing classes?

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 ?

Upvotes: 2

Views: 2849

Answers (2)

Compass
Compass

Reputation: 5937

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.

Upvotes: 2

Harry Coder
Harry Coder

Reputation: 2740

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.

Upvotes: 1

Related Questions