Reputation: 1759
Let's pretend that I have an interface called IValidator
that looks like the following:
public interface IValidator {
/**
* Returns true if the specified strings are valid.
*/
public boolean validate(List<String> someStrings);
}
Now let's say that I have two implementations of IValidator
:
public class StrictValidator implements IValidator {
public boolean validate(List<String> someStrings) {
//some strict validation code
return false;
}
}
public class LaissezFaireValidator implements IValidator {
public boolean validate(List<String> someStrings) {
//some easy-going validation code
return true;
}
}
Now let's add a servlet that uses an injected instance of IValidator
:
@Service
@At("/rest")
public class MyServlet extends AbstractServlet {
private final IValidator validator;
@Inject
public MyServlet(final IValidator validator) {
this.validator = validator;
}
@Post
@At("/validate")
@LaissezFaire
public Reply<?> validate(Request request) {
//get the strings to validate out of the request object
List<String> strings = (List<String>) restUtil.parseRequest(request, List.class);
//validate the request
if (!this.validator.validate(strings)) {
return Reply.saying().status(409);
} else {
return Reply.saying().noContent();
}
}
}
Of course we'll also need to bind IValidator
to StrictValidator
in a module:
public class ValidatorModule implements Module {
@Override
protected void configure() {
bind(IValiator.class).to(StrictValidator.class);
}
}
But what happens if I want to conditionally bind IValidator
to StrictValidator
in one case, but instead bind it to LaissezFaireValidator
in some other case?
Did you notice the @LaissezFaire
annotation on MyServlet.validate
above? That's an interceptor that looks like this:
@BindingAnnotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LaissezFaire { }
public class LaissezFaireInterceptor implements MethodInterceptor {
private boolean debug;
private IValidator validator;
@Inject
public void setDebug(@Named("debug.enabled") boolean debugEnabled) {
this.debug = debugEnabled;
}
@Inject
public void setValidator(final IValidator validator) {
this.validator = validator;
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
if (debug) {
if (!this.validator.validate(strings)) {
return Reply.saying().status(409);
} else {
return Reply.saying().noContent();
}
} else {
return invocation.proceed();
}
}
}
And once again we need some bindings to set up the interceptor:
public class InterceptorModule implements Module {
@Override
protected void configure() {
final MethodInterceptor lfInterceptor = new LaissezFaireInterceptor();
requestInjection(lfInterceptor);
bindInterceptor(Matchers.subclassesOf(AbstractServlet.class), Matchers.AnnotatedWith(LaissezFaire.class), lfInterceptor);
}
}
According to the ValidatorModule
, the LaissezFaireInterceptor
class will get an instance of StrictValidator
when InterceptorModule
calls requestInjection(lfInterceptor);
.
Instead, I'd like MyServlet
to get an instance of StrictValidator
and LaissezFaireInterceptor
to get an instance of LaissezFaireValidator
.
According to the Google Guice docs, I could use a named annotation when I request injection. The constructor of MyServlet
would be modified to look like the following:
@Inject
public MyServlet(@Named("strict") final IValidator validator) {
this.validator = validator;
}
and the setValidator
method of LaissezFaireInterceptor
would be modified to look like the following:
@Inject
public void setValidator(@Named("laissezfaire") final IValidator validator) {
this.validator = validator;
}
and finally ValidatorModule
would be modified to look like the following:
public class ValidatorModule implements Module {
@Override
protected void configure() {
bind(IValiator.class).annotatedWith(Names.named("strict")).to(StrictValidator.class);
bind(IValidator.class).annotatedWith(Names.named("laissezfaire")).to(LaissezFaireValidator.class);
}
}
This is all well and good except that the docs specifically say to avoid this approach because the string names can't be checked by the compiler. In addition, it means that I have to add an @Named
annotation to every place in the code that requests an IValidator
by injection, or else the binding will fail.
I had really hoped that Provider Bindings could solve this problem for me, but they don't appear to know anything about the context in which the binding is being made. Since they don't know the type of the class that is requesting the binding, I can't choose which type of IValidator
to return from the get()
method.
Is there a better way to approach this problem?
Upvotes: 0
Views: 876
Reputation: 1759
While Condit supplied some excellent suggestions, we opted to solve this problem with a more straightforward solution.
As above, we created the IValidator
interface, as well as the StrictValidator
and LaissezFaireValidator
classes. We used the ValidatorModule
to bind IValidator
to StrictValidator
in the default case. As a reminder, it looks like this:
public class ValidatorModule implements Module {
@Override
protected void configure() {
//in the default case, inject an instance of StrictValidator
bind(IValiator.class).to(StrictValidator.class);
}
}
In the vast majority of cases, StrictValidator
is the required implementation, as the LaissezFaireInterceptor
is a cheat that is used for testing.
Wherever we wanted a StrictValidator
(like we do in MyServlet
), we injected an instance of IValidator
:
public class MyServlet extends AbstractServlet {
private final IValidator validator;
@Inject
public MyServlet(final IValidator validator) {
this.validator = validator;
}
//... there's more code here (look above) ...
}
And wherever we wanted an instance of LaissezFaireValidator
, we asked for its concrete implementation to be injected in place of IValidator
:
public class LaissezFaireInterceptor implements MethodInterceptor {
private final IValidator validator;
//... a bunch of other code goes here (see above) ...
@Inject
public void setValidator(final LaissezFaireValidator validator) {
this.validator = validator;
}
//... and a bunch more code goes here (again, see above) ...
}
In this way, we were able to conditionally inject the required implementation based on the context of the injection without introducing any extra annotations or factories.
Sure, it's not as Guicy as it could be, but it works.
Upvotes: 1