Guice with multiple concretes......picking one of them
Asked Answered
M

1

0

I am injection multiple concretes of the same interface.

I figured out the Guide "code it up" convention.

My code currently spits out

[INFO] App - About to ship. (abc)
[INFO] App - ShipperInterface . (FedExShipper)
[INFO] App - ShipperInterface . (UpsShipper)
[INFO] App - ShipperInterface . (UspsShipper)

So I have the multiple "shippers" at my fingertips.

Note the method:

public void ProcessOrder(String preferredShipperAbbreviation, Order ord) {

I'm trying to figure out the best way to use the (String) preferredShipperAbbreviation to choose 1 of the 3 concrete shippers.

Is there a way to "name" my 3 concretes when I register them with Guice?

Or what is the best way to pick 1 of the three ?

public class ProductionInjectModule extends AbstractModule implements Module {

  @Override
  protected void configure() {
    try {
      bind(OrderProcessorInterface.class).toConstructor(OrderProcessorImpl.class.getConstructor(Set.class));

      Multibinder<ShipperInterface> multibinder = Multibinder.newSetBinder(binder(), ShipperInterface.class);
      multibinder.addBinding().toConstructor(FedExShipper.class.getConstructor(org.apache.commons.logging.Log.class));
      multibinder.addBinding().toConstructor(UpsShipper.class.getConstructor(org.apache.commons.logging.Log.class));
      multibinder.addBinding().toConstructor(UspsShipper.class.getConstructor(org.apache.commons.logging.Log.class));

    } catch (NoSuchMethodException e) {
      addError(e);
    }
  }

}

=============

import java.util.Collection;
import java.util.Set;

import org.apache.commons.logging.Log;


public class OrderProcessorImpl implements OrderProcessorInterface {

  private Log logger;
  Set<ShipperInterface> shippers;

  public OrderProcessorImpl(Log lgr, Set<ShipperInterface> shprs) {

    if (null == lgr) {
      throw new IllegalArgumentException("Log is null");
    }

    if (null == shprs) {
      throw new IllegalArgumentException("ShipperInterface(s) is null");
    }

    this.logger = lgr;
    this.shippers = shprs;
  }

  public void ProcessOrder(String preferredShipperAbbreviation, Order ord) {
    this.logger.info(String.format("About to ship. (%1s)", preferredShipperAbbreviation));

    for (ShipperInterface sh : shippers) {
      this.logger.info(String.format("ShipperInterface . (%1s)", sh.getClass().getSimpleName()));
    }

  }
}

=============

public interface OrderProcessorInterface {

  void ProcessOrder(String preferredShipperAbbreviation, Order ord);

}

public class FedExShipper implements ShipperInterface {

  private Log logger;

  public FedExShipper(Log lgr) {

    if (null == lgr) {
      throw new IllegalArgumentException("Log is null");
    }

    this.logger = lgr;
  }

  public void ShipOrder(Order ord) {
    this.logger.info("I'm shipping the Order with FexEx");
  }
}


public class UpsShipper implements ShipperInterface {

  private Log logger;

  public UpsShipper(Log lgr) {

    if (null == lgr) {
      throw new IllegalArgumentException("Log is null");
    }

    this.logger = lgr;
  }

  public void ShipOrder(Order ord) {
    this.logger.info("I'm shipping the Order with Ups");
  }
}


public class UspsShipper implements ShipperInterface {

  private Log logger;

  public UspsShipper(Log lgr) {

    if (null == lgr) {
      throw new IllegalArgumentException("Log is null");
    }

    this.logger = lgr;
  }

  public void ShipOrder(Order ord) {
    this.logger.info("I'm shipping the Order with Usps");
  }
}

..............

"Main" method:

ProductionInjectModule pm = new ProductionInjectModule();
Injector injector = Guice.createInjector(pm);

Order ord = new Order();
OrderProcessorInterface opi = injector.getInstance(OrderProcessorInterface.class);
opi.ProcessOrder("WhatDoIPutHere?", ord);

=========== Guice version below:

    <dependency>
        <groupId>com.google.inject</groupId>
        <artifactId>guice</artifactId>
        <version>4.2.0</version>
    </dependency>

================================

One way I'm trying it this way. Is this as good as any way?

Ultimately, in my "real" scenario (not this made up one)......I want to keep the "concreteKey" as a database/configuration setting.

Order ord = new Order();
OrderProcessorInterface opi = injector.getInstance(OrderProcessorInterface.class);
opi.ProcessOrder(FedExShipper.class.getSimpleName(), ord);

  public void ProcessOrder(String preferredShipperAbbreviation, Order ord) {
    this.logger.info(String.format("About to ship. (%1s)", preferredShipperAbbreviation));

    ShipperInterface foundShipperInterface = this.FindShipperInterface(preferredShipperAbbreviation);
    foundShipperInterface.ShipOrder(ord);
  }

  private ShipperInterface FindShipperInterface(String preferredShipperAbbreviation) {

    /* requires java 8 */
    ShipperInterface foundShipperInterface = this.shippers
        .stream().filter(x -> x.getClass().getSimpleName().equalsIgnoreCase(preferredShipperAbbreviation)).findFirst().orElse(null);

    if(null == foundShipperInterface)
    {
      throw new NullPointerException(String.format("ShipperInterface not found in ShipperInterface collection. ('%1s')", preferredShipperAbbreviation));
    }

    return foundShipperInterface;
  }

============= APPEND ==================

I got this to work thanks to Jeff B's answer/comments.

import java.util.Map;
import java.util.Set;

import com.google.inject.AbstractModule;
import com.google.inject.Module;
import com.google.inject.multibindings.MapBinder;
import com.google.inject.multibindings.Multibinder;

public class ProductionInjectModule extends AbstractModule implements Module {

  @Override
  protected void configure() {
    try {

      MapBinder<String, ShipperInterface> mappyBinder = MapBinder.newMapBinder(binder(), String.class, ShipperInterface.class);
      mappyBinder.addBinding("myFedExName").toConstructor(FedExShipper.class.getConstructor(org.apache.commons.logging.Log.class));
      mappyBinder.addBinding("myUPSName").toConstructor(UpsShipper.class.getConstructor(org.apache.commons.logging.Log.class));
      mappyBinder.addBinding("myUSPSName").toConstructor(UspsShipper.class.getConstructor(org.apache.commons.logging.Log.class));

        /* below is not needed, but shows what needs to be injected */
      java.util.Map<String,  javax.inject.Provider<ShipperInterface>> shipperProviderMap;


    } catch (NoSuchMethodException e) {
      addError(e);
    }
  }
}

================

import java.util.Collection;
import java.util.Set;

import org.apache.commons.logging.Log;

public class OrderProcessorImpl implements OrderProcessorInterface {

  private Log logger;
  private java.util.Map<String, javax.inject.Provider<ShipperInterface>> shipperProviderMap;

  public OrderProcessorImpl(Log lgr, java.util.Map<String, javax.inject.Provider<ShipperInterface>> spMap) {

    if (null == lgr) {
      throw new IllegalArgumentException("Log is null");
    }

    if (null == spMap) {
      throw new IllegalArgumentException("Provider<ShipperInterface> is null");
    }

    this.logger = lgr;
    this.shipperProviderMap = spMap;
  }

  public void ProcessOrder(String preferredShipperAbbreviation, Order ord) {
    this.logger.info(String.format("About to ship. (%1s)", preferredShipperAbbreviation));


    ShipperInterface foundShipperInterface = this.FindShipperInterface(preferredShipperAbbreviation);
    foundShipperInterface.ShipOrder(ord);
  }

  private ShipperInterface FindShipperInterface(String preferredShipperAbbreviation) {

    ShipperInterface foundShipperInterface = this.shipperProviderMap.get(preferredShipperAbbreviation).get();

    if (null == foundShipperInterface) {
      throw new NullPointerException(
          String.format("ShipperInterface not found in shipperProviderMap. ('%1s')", preferredShipperAbbreviation));
    }

    return foundShipperInterface;
  }
}

================

"main" method

ProductionInjectModule pm = new ProductionInjectModule();
Injector injector = Guice.createInjector(pm);

Order ord = new Order();
OrderProcessorInterface opi = injector.getInstance(OrderProcessorInterface.class);
opi.ProcessOrder("myFedExName", ord); /* now use the "friendly named" strings */

OUTPUT:

[INFO] App - About to ship. (myFedExName)
[INFO] App - I'm shipping the Order with FexEx

I probably have some extra "logger" injections in my newly posted code.....but simple clean up would get it running.

Mihe answered 11/9, 2018 at 15:30 Comment(1)
Future readers, I have a Spring IoC / DI example here too : #52338822Mihe
R
4

If you use Multibinder for map bindings, then you could bind each of the Shipper instances into a Map using MapBinder:

MapBinder<String, ShipperInterface> multibinder = MapBinder.newMapBinder(
    binder(), String.class, ShipperInterface.class);
multibinder.addBinding("FedEx").to(FedExShipper.class);
multibinder.addBinding("UPS").to(UpsShipper.class);
multibinder.addBinding("USPS").to(UspsShipper.class);

Then in your injected class you can inject a Map<String, Provider<ShipperInterface>>:

private ShipperInterface FindShipperInterface(String 
    preferredShipperAbbreviation) {

  ShipperInterface foundShipperInterface =
      providerMap.get(preferredShipperAbbreviation).get();
}

You could also inject a Map<String, ShipperInterface> directly, but Multibinder handles the Provider indirection for free, which lets you avoid creating three ShipperInterface instances when only one will actually be necessary. Also, if your instance-selection code is more complicated than simply choosing based on a String from a set of implementations you know at compile time, you might still want a Factory implementation you write.


As a side note, ideally use @Inject annotations and bind(...).to(...) instead of toConstructor. This doesn't tie you to Guice, because @Inject is defined in JSR-330, and you are adding annotations that you can choose not to use later. You can also write a @Provides method in your AbstractModule, like so, which is no more fragile than your toConstructor bindings:

@Provides UspsShipper provideUspsShipper(Log log) {
  return new UspsShipper(log);
}

Use toConstructor if and only if you are using legacy code, code you don't control, very restrictive code style rules, or AOP (which may be the case here). I've done so above for the sake of a concise example, but you can revert to toConstructor if necessary.

Reserved answered 11/9, 2018 at 20:3 Comment(12)
Thanks. I'm trying this out now. As far as the @Annotation, I guess I'm a "DI purist"....and don't think annotations (attributes in c#) should be needed anywhere. Microsoft Unity "configuration by coding it up" does not request any annotations.......and it defaults to using the ~most arguments constructor~ as its "go to" constructor for DI. (its possible to override with an attribute (annotation in java)...but the default mode is to use the most-arguments-constructor) (kinda makes sense to me). Anyways. Just food for thought.Mihe
I love Guice (over spring)....but wish it had this "default to most arguments constructor" functionality like Unity does. But I think being a 'purist' is a small population.Mihe
Hi. What is the full namespace/package name for "MapBinder" ?Mihe
MapBinder is one of the Multibindings family of classes; it's com.google.inject.multibindings.MapBinder. (I got the construction wrong above, sorry!) I would strongly disagree that "defaulting to the most arguments" is "purist" behavior. If anything, it favors assumption over convention or configuration: an updated dependency you inject could cause an obscure breakage. In any case, Guice is heavily annotations-based, and Dagger even more so, so idiomatic use requires annotations.Reserved
Ha ha! I know I'm in a minority. I don't like any (at)Annotations on my concretes. But I am warming up to "(at)Inject is defined in JSR-330" (second time I've seen this) .. since its not framework specific. (at)Autowire makes me shake. I'm trying the updated code now. Thanks.Mihe
Ah. I would have to inject "java.util.Map<String, com.google.inject.Provider<ShipperInterface>> shipperProviderMap" into the concrete that needs the multiple shippers. That would mean I have DI objects (com.google.inject.Provider) in my code. My "keep it clean" attitude is killing me. If no one else pipes in, I'll mark this as answer, since this was a great learning exercise. But I'll let it set for a bit. THANKS JEFF for giving me another angle to come at this.Mihe
@Mihe Good news for that one: You can use javax.inject.Provider, which com.google.inject.Provider extends anyway since Guice's one predates the JSR330 one. You can see the case handled in the code, too. Full details on the JSR330 Guice wiki page. (Also, thanks for the code fix!)Reserved
javax.inject.Provider might be the ticket! I'm now working on that one. You rock the suburbs! and you give the reason why there are two ("since Guice's one predates the JSR330") hashtag-BAM. My code fix proves I was actually trying it!Mihe
I got the magic error "Classes must have either one (and only one) constructor annotated with @Inject or a zero-argument constructor that is not private." When I see this, I always say to myself "use most-arguments-constructor like Unity" :( (haha)Mihe
My question appended with the "answer" you inspired. Thanks for input! I can go home and prepare for hurricane florence now.Mihe
I just tried getting rid of "toConstructor" and using (at)javax.inject.Inject .. as in (at)javax.inject.Inject public FedExShipper(Log lgr) { if (null == lgr) { throw new IllegalArgumentException("Log is null"); } this.logger = lgr; } I gotta sleep on it!Mihe
I'm talking to another coworker. We're both like "If the class has only ONE constructor (with arguments)........Guice should auto-use that one..without (at)javax.inject.Inject annotation. Only make us use "(at)javax.inject.Inject" if there is more than one constructor. Now I'm going home for the day!Mihe

© 2022 - 2024 — McMap. All rights reserved.