MusikPolice
MusikPolice

Reputation: 1759

Contextually Binding Two Different Interface Implementations with Google Guice

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

Answers (1)

MusikPolice
MusikPolice

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

Related Questions