Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: ConditionalBinder #256

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open

Conversation

elandau
Copy link
Contributor

@elandau elandau commented Dec 11, 2015

The ConditionalBinder enables specifying multiple candidates for a binding with each having a condition that can be based on configuration or other state. ConditionalBinder follows an implementation very similar to Multibinder in that all bindings are created with custom annotations and are resolved at injector creation time.

For example,

public class SnacksModule extends AbstractModule {
    protected void configure() {
        ConditionalBinder<Snack> conditionalbinder
          = ConditionalBinder.newConditionalBinder(binder(), Snack.class);
        conditionalbinder.when(new ConditionalOnProperty("type", "twix")).toInstance(new Twix());
        conditionalbinder.when(new ConditionalOnProperty("type", "snickers")).toProvider(SnickersProvider.class);
        conditionalbinder.when(new ConditionalOnProperty("type", "skittles")).to(Skittles.class);
        conditionalbinder.whenNone().to(Carrots.class);
     }
}

The following situations are not valid and result in a CreationException error at injector creation (not injection) time. The exception will contain detailed information on the candidate bindings and where in code they were bound

  • Multiple matching candidates
  • No matched candidates
  • Multiple default bindings specified
  • Using conditional binding in Stage.PRODUCTION

Several approaches were considered for implementing this feature, including a Module annotation based approach. The benefits of this approach are,

  • Follows Guice conventions (such as Multibinder and MapBinder)
  • Does not require Module post processing (which would complicate usage)
  • Makes conditional processing injectable

Drawbacks to this approach are,

  • Does not work in Stage.PRODUCTION (results in unmatched conditionals being instantiated). Allowing this to run in production stage is possible but would add complexity that may not be necessary since we run only in the lazy Stage.DEVELOPMENT. We can revisit this if running in Stage.PRODUCTION becomes are requirement.
  • Unmatched conditionals result in unused bindings

* <p>Scoping elements independently supported per binding. Use the {@code in} method
* to specify a binding scope.
*/
public abstract LinkedBindingBuilder<T> whenMatch(Conditional obj);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about adding operators to compose Conditionals (and, or/any, not)? Implementing this explicitly (by wrapping into higher level Conditional object) would be cumbersome, due to the way injections are handled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guice's Matcher has an abstract implementation that includes 'and' and 'or'. We could follow the same pattern here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the PR with support for AND, OR and NOT operations on conditionals. I also simplified the API by removing the Matcher class and making dependencies required for evaluating a conditional directly injectable into the conditional via member injection.

…njection of any dependency needed by the conditional directly into the conditional. Add AND, OR, NOT operations on conditionals.
* <p>Scoping elements independently supported per binding. Use the {@code in} method
* to specify a binding scope.
*/
public abstract LinkedBindingBuilder<T> whenNoMatch();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we do this with whenMatch(new DefaultBindingConditional()) or something like that to avoid the special case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a very long and ambiguous way of specifying the default and is not in the sprit of a DSL. DefaultBindingConditional could mean several things, is it the default implementation of a conditional or is it the conditional to use as the default.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whenNone is just another conditional though, maybe call it whenMatch(new ConditionalOnNoOtherMatches())?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either way, ConditionalOnNoOtherMatches() would require special handling since it would require knowledge of how all other conditionals have been processed. IMO it's much cleaner to just track this as an explicit special default case.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do I do something like whenMatch(Conditional.and(new ConditionOnNoOtherMatches(), new ConditionalOnProfile("prod")) if this is kept as a special case?

…will then be the responsibility of a composite conditional (AndConditional, etc.) to pass the Injector to its children, giving them access to the injector.
…ls class. Rename 'and' to 'allOf', and 'or' to 'anyOf'.
*/
public interface Conditional {
/**
* Evaluate whether the condition is true. evaluate() is only called once at injector
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

javadoc update

import com.google.inject.Module;
import com.google.inject.Provides;
import com.netflix.governator.spi.PropertySource;

public class PropertiesPropertySource extends AbstractPropertySource {
public final class PropertiesPropertySource extends AbstractPropertySource implements Module {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gak. Properties is legacy Java with all synchronized methods. Why do we want this in 2016, and not a Map<String,String>?

if (getClass() != obj.getClass())
return false;

throw new RuntimeException("Only one PropertiesModule may be installed");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IllegalStateException would be clearer; usually a bad idea to throw RuntimeException

public class AllOfConditional implements Conditional {
private final List<Conditional> children;

public AllOfConditional(List<Conditional> children) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be able to take any Iterable or Collection as input, and add a varargs constructor too?

import java.util.List;
import java.util.Set;

import com.google.common.collect.ImmutableList;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to use these google collections classes any more (with Java 8)?


static <T> ConditionalBinder<T> newRealConditionalBinder(Binder binder, Key<T> key) {
if (binder.currentStage().equals(Stage.PRODUCTION)) {
throw new RuntimeException("ConditionalBinder may not be used in Stage.PRODUCTION. Use Stage.DEVELOPMENT.");
Copy link
Contributor

@tcellucci tcellucci Nov 22, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IllegalStateException or IllegalArgumentException, take your pick

@Override
public List<Binding<?>> getCandidateElements() {
if (isInitialized()) {
return (List<Binding<?>>) (List<?>) candidateBindings; // safe because bindings is immutable.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

double cast?

}

@Override
public boolean equals(Object o) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems flawed that Condition is not part of the equality check, but is part of the object state and passed in via constructor. I would include at least an identity comparison for Condition here.

/**
* Utility class for creating conditional
*/
public abstract class Conditionals {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for 'abstract', it's not instantiable (using java.util.Collections for comparison)


@Singleton
static class SingletonFoo implements Foo {
static int injectedCount = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

volatile?

@tcellucci
Copy link
Contributor

tcellucci commented Nov 22, 2016

I didn't see any unit tests that use the @conditional[Anything]; annotation-based conditionals are supported yes?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants