1

Say we have an interface like this one:

public interface Validator<T> {
  boolean isValid(T data);
}

And this is part of a core module. Multiple apps can use that same core module, with a different value for the generic T. An example implementation is (from a specific module of the application):

@Component
public class AppValidator implements Validator<String> {
  @Override
  public boolean isValid(String data) {
    return false;
  }
}

And then in the controller (which is part of the core module):

@RestController
public class ValidateController {
  @Autowired
  private Validator validator;

  @RequestMapping("/")
  public void index() {
    validator.validate("");
  }
}

IntelliJ is complaining that I'm using raw types; as you can see, I'm actually doing that in the controller.

My question: is there a way to inject the dependency in a bounded way (instead of injecting Validator, injecting Validator<String>)? But of course, the bound type could change depending on the application using the core module?

If not possible (probably due to type erasure), what's the best practice for this? Is it just to use Object? Is there no nicer alternative that still provides type-safety?

I've seen somewhere people saying it's possible to do some magic at compile-time to change the types, but I'm not sure how, or even if I read it correctly?

I am using Spring, so I'm hoping Spring can provide something to help me here! Some magic is welcome!

Will
  • 181
  • 1
  • 14
  • "what's the best practice for this?" ... inject what you need. You need `AppValidator` not some random Validator that can do something with String. – Tom Jun 27 '19 at 22:52
  • @Tom In practice yes. But here the OP made the controller a common component. That is used by applications and not the reverse way. – davidxxx Jun 28 '19 at 07:42

2 Answers2

2

The answer is simple: you don't need any magic, it just works in Spring. You have your AppValidator and then you just do (it's injected by looking at generic type):

@Autowired 
private Validator<String> appValidator;

Well it's all fine, now imagine you have two Validator<String>, what then? required a single bean but found two exception - that's what. That's why it's a horrible practice, don't ever ever do that.

At my work one person created generic interface with 3 generic types and then based injection on those generic types, people still hate him. It looked like this, and yes, it works, as long as you don't have the exact same 3 generic types in the exact same order in multiple implementations:

@Autowired
private Invoker<String, Integer, Person> personInvoker;

@Autowired
private Invoker<Integer, String, Animal> animalInvoker;

Even if you don't have multiple Validator<String> in your code, and you don't plan on having more - someone else may come in and add them, or many other scenarios.

Shadov
  • 5,421
  • 2
  • 19
  • 38
  • In these cases explicitly naming the bean and using @Qualifier where it's injected clarifies which bean instance is to be used. – Andrew S Jun 27 '19 at 23:43
  • Yes, and people usually go for this solution instead of fixing their poor design. The example from my answer did that aswell. There was 49 implementations of this interface and they had some overlapping generic types - `@Qualifier("stringIntegerPersonInvoker")` to the rescue – Shadov Jun 28 '19 at 00:06
  • Maybe I have not explained it properly, but in this case you're injecting `Validator`, but that's not really what I want? Some other app may need `Validator`, and another app may need something else; it's variable, I cannot hardcode it. Basically, I can have two different apps using that same codebase, but one could be validating with String and another one with Integer. I need the core to be abstracted away from that? – Will Jun 28 '19 at 06:51
1

Here the relationships between your modules (applications and core) :

Application 1       Application 2      Application 3
     |                   |                   |
Validator<Foo>     Validator<Bar>     Validator<FooBar>
     |                   |                   |  
     |                   |                   |  
     |__ __ __ __ __ __ _| __ __ __ __ __ __ | 
                         |
                         | <<uses>>
                         |
                        \ /
                     Core Module    
                         |
                 ValidateController  (not generic rest controller)                   

Something is wrong here since you want that a shared component ValidateController relies on a specific application generic Validator class but ValidateController is not a generic class, so you could only stick with Object as generic type where you will use the Validator field.
To make things consistent, you should create this missing link. In fact you need distinct subclasses of controller because each controller needs to use a specific instance of validator.
You could for example define an abstract class/interface ValidateController in the shared/code module and leave each subclass extends it and defines itself the generic Validator class to use.

Here the target relationships between your modules :

Application 1        Application 2        Application 3
     |                   |                      |
Validator<Foo>       Validator<Bar>       Validator<FooBar>
FooController(bean)  BarController(bean)  FooBarController(bean)
     |                   |                      |  
     |                   |                      |  
     |__ __ __ __ __ ___ | __ ___ __ __ __ __ __| 
                         |
                         | <<uses>>
                         |
                        \ /
                     Core Module    
                         |
                 ValidateController<T>  (abstract class and not a bean)                   

For example in the core/shared module :

public abstract class ValidateController<T> {

  private Validator<T> validator;

  ValidateController(Validator<T> validator){
     this.validator = validator;
  }

  @RequestMapping("/")
  public void index(T t) {
    boolean isValid = validator.validate(t);
  }

}

In the application, define your validator implementation :

@Component
public class AppValidator implements Validator<String> {
  @Override
  public boolean validate(String data) {
    return ...;
  }
}

And define also the StringController subclass (or @Bean as alternative) to set the right Validator :

@RestController
public class StringController extends ValidateController<String>{

   public ValidateControllerApp(Validator<String> validator){
       this.validator = validator;
   }

}
davidxxx
  • 125,838
  • 23
  • 214
  • 215
  • Wow I think this is the answer. Let me give it a go. Basically you're saying that each application should extend the actual controller and specify the real type. Great one; let me give it a quick try – Will Jun 28 '19 at 06:54
  • That is exactly that. You need distinct subclass of controller because each controller needs to use a specific instance of validator. – davidxxx Jun 28 '19 at 07:04
  • I accepted your answer, thank you very much. Just another question if you don't mind: is there a way to actually create this boilerplate for you easily? I know it's just a simple class extending another one, but I may then have a service that also is a generic (therefore I'll have to create the base service...). Is there a way to "automatically" do this? Thanks a lot – Will Jun 28 '19 at 07:09
  • You are welcome. You have some ways. For example you could rely on the same strategy than `JpaRepository` that relieves you from declaring any constructor. Note that using this strategy takes some time to implement : it is not provided out of the box. But you could not go further in terms of boiler plate reduction without making your design opaque. – davidxxx Jun 28 '19 at 07:12
  • Understood. Thank you very much for your help, I approved it and added a +1 to your answer. Now I'll go back and work at it! :) – Will Jun 28 '19 at 07:13
  • You are welcome. Glad if I could help you in your path :) See you – davidxxx Jun 28 '19 at 07:15