Search code examples
javaspringunit-testingdesign-patternsdependency-injection

Dependency injection how to avoid hiding dependencies to stateful objects like a validator


I would like to have your opinion about such a situation, the code is quite simple to should be easy to follow:

@Component
class MyService {
    
    AusiliaryService ausService;

    public MyService(AusiliaryService ausService) {
        ...
    }

    public boolean isAccountValid(Account account) {

     AccountValidator accountValidator = new AccountValidator(account);
     boolean isValid = accountValidator.isValid(account);
     if(isValid) {
        ausService.openAccount(account);
     }
    }
}

Note that AusiliaryService is also spring managed and we are using constructor injection.

In this class AccountValidator is a dependency of MyService but it does not appear in the constructor so we could say the visibility of this dependency is somehow hidden.

Since we are hardcoding the instantiation of
AccountValidator accountValidator = new AccountValidator(account);

We may also have some potential issues with writing unit tests MyService, unless you may think such any unit tests should also impact AccountValidator, which for me would be a valid line of thought.

A possible work around for both "issues" would be to use a factory, to remove from MyService the responsability to instantiate AccountValidator object.

@Component
class MyService {
    
    AusiliaryService ausService;
    AccountValidatorFactory accountValidatorFactory;

    public MyService(AusiliaryService ausService, AccountValidatorFactory accountValidatorFactory;) {
        ...
    }

    public boolean isAccountValid(Account account) {

     AccountValidator accountValidator = accountValidatorFactory.getValidator(account);
     boolean isValid = accountValidator.isValid(account);
     if(isValid) {
        ausService.openAccount(account);
     }
    }
}

Note that also AccountValidatorFactory is spring managed and we are using constructor injection.

A cons of this solution could be though the proliferation of such factories classes, if you use this kind of pattern often in your code.

What would is your approach in such situations ? Which kind of solution would you prefer ? Would be possible to make AccountValidator also spring managed to help with this, how would that be done ?


Solution

  • The simple answer is that you can just make AccountValidator a managed spring bean. You can accomplish this the same way that you made this class (MyService), or how you would have made the factory classes you describe, managed - by adding @Component.

    If you need to preserve the semantics, e.g. if this bean is not thread safe and you need to create new ones every time, you can make it a protype bean.

    Here is an example, and you can see that it is being injected in a way that is using spring libraries without your own factories. it will print:

    Example.MyValidator(id=0)
    Example.MyValidator(id=1)
    Example.MyValidator(id=2)
    

    here is the code:

    package org.example;
    
    import lombok.ToString;
    import org.springframework.beans.factory.ObjectProvider;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.config.ConfigurableBeanFactory;
    import org.springframework.boot.ApplicationRunner;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.ConfigurableApplicationContext;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Scope;
    import org.springframework.stereotype.Component;
    import org.springframework.stereotype.Service;
    
    import java.util.concurrent.atomic.AtomicInteger;
    
    @SpringBootApplication
    public class Example {
        public static void main(String[] args) {
            SpringApplication.run(Example.class, args);
        }
    
        @Autowired
        MyService myService;
        @Autowired
        ConfigurableApplicationContext context;
    
        @Bean
        ApplicationRunner applicationRunner() {
            return args -> {
                for (int i = 0; i < 3; i++) {
                    System.out.println(myService.state());
                }
                context.close();
            };
        }
    
        @Service
        static class MyService {
            private final ObjectProvider<MyValidator> myValidators;
    
            MyService(ObjectProvider<MyValidator> myValidators) {
                this.myValidators = myValidators;
            }
    
            String state() {
                return myValidators.getObject().toString();
            }
        }
    
        @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
        @Component
        @ToString
        static class MyValidator {
            private static final AtomicInteger counter = new AtomicInteger();
            private final int id;
    
            MyValidator() {
                this.id = counter.getAndIncrement();
            }
        }
    }