I'm using Google Guice for dependency injection. Suppose I have the following:
public interface Payment {
public void pay();
}
public class PaymentCardImpl implements Payment {
public void pay() {
System.out.println("I pay with a card");
}
}
public class PaymentCashImpl implements Payment {
public void pay() {
System.out.println("I pay cash");
}
}
public class Order {
private Payment payment;
@Inject
public Order(Payment payment){
this.payment=payment;
}
public void finishOrder(){
this.payment.pay();
}
}
Following on from this, is a very simple module for binding, like so:
public class MyModule extends AbstractModule {
@Override
protected void configure() {
bind(Payment.class).to(PaymentCashImpl.class);
}
}
As you can see, a Payment
instance is injected into the Order constructor. This is done in the MyModule
class and overall is really cool.
My main looks like:
public static void main(String[] args) {
MyModule module = new MyModule();
Injector injector = Guice.createInjector(module);
Order order = injector.getInstance(Order.class);
order.finishOrder();
}
What I can't see however, is how I could incorporate some way to conditionally bind either a PaymentCardImpl
or a PaymentCashImpl
instance to the Order
constructor.
Let's say for example, that the order was an 'online' order. I would then need to this:
bind(Payment.class).to(PaymentCardImpl.class);
What's the best way to do this? I'm new to dependency injection.
Using MapBinder extension you can get injected with several bindings contained in a Map
. Then it's a matter of implementation which one to use.
You will need a key, in your case that could be a String
or Enumeration
and bind all Payment
implementations to the appropriate keys:
public class MyModule extends AbstractModule {
@Override
protected void configure() {
// this one aggregates all bindings and could be further annotated if you
// need several maps with same key/value types
MapBinder<String, Payment> mapBinder = MapBinder.newMapBinder(binder(),
String.class, Payment.class);
// simple binding of PaymentCashImpl to 'cash' key
mapBinder.addBinding("cash").to(PaymentCashImpl.class);
// you can scope during binding or using @Singleton annotation on implementations
mapBinder.addBinding("card").to(PaymentCardImpl.class).in(Singleton.class);
}
}
Then in Order
you get injected with the whole map and decide which implementation to use:
public class Order {
@Inject
Map<String, Provider<Payment>> paymentProcessors;
public void finishOrder(String paymentMethod) {
if (!paymentProcessors.containsKey(paymentMethod)) {
throw new IllegalArgumentException("Unknown payment method " + paymentMethod);
}
Payment paymentProcessor = paymentProcessors.get(paymentMethod).get();
// do your stuff...
paymentProcessor.pay();
}
}
Note: Injecting providers doesn't work with javax.inject.Provider
, you need to use com.google.inject.Provider
.
By injecting providers instead of instances you can achieve lazy instantiation and different instantiation policies per implementation. Like in the example above the PaymentCardImpl
is singleton, while the other is created each time. You can use guice scopes to control the lifespan, or even implement your own provider to accomplish something else.
You can annotate which one you want to inject. If you do a named binding it will resolve the issue.
See below:
bind(Payment.class).annotatedWith(Names.named("Card")).to(PaymentCardImpl.class);
bind(Payment.class).annotatedWith(Names.named("Cash")).to(PaymentCashImpl.class);
Then where you want to inject you do:
@Named("Cash") Payment payment
or:
@Named("Card") Payment payment
Dependency injection is useful for creating service
style objects. These have the following characteristics:-
Based on this, Payment
is a service object. I would rename it PaymentService
to distinguish it from the ledger entry you may store about a payment (which would be a value object).
You example does not show much about what the Order
class, does, but I assume it would hold information like some line items, delivery address and total amount. This is a value
object. It represents a thing in the business domain.
Value objects are heavy on state, and lighter on behaviour. Multiple implementations are possible, but you are less likely to want to replace one implementation with another.
Value objects are not created by your dependency injection framework. They are created by your business logic code. In your example, you are creating all the objects using Guice. I expect in reality you would need to create the Order
at run-time based on user input.
Service objects can depend on value objects, but never the other way around. I think you should be looking to implement:
checkoutService.payfor( order, method );
and not order.finishOrder( method )
Within the CheckoutService
class, you could choose an approriate PaymentService
and pass the order
to it. The CheckoutService
would take as constructor arguments PaymentCardPaymentService
(equivalent to your PaymentCardImpl) and CashPaymentService
(equivalent to your PaymentCashImpl).
I know why you want to do this. But I would not mix up construction code (which is dependency injection configuration) with business logic. If you do that, your business logic might not be understandable anymore. And to me it seems, that your conditional injection depends on the situation, i.e. an input from a user interface.
So why not just inject both and make the condition explicit? I'd prefer that. An example app:
public class MyModule extends AbstractModule {
@Override
protected void configure() {
}
public static void main(String[] args) {
MyModule module = new MyModule();
Injector injector = Guice.createInjector(module);
Order order = injector.getInstance(Order.class);
order.finishOrder(PaymentMethod.CARD);
}
}
public class PaymentProvider {
private final Payment cashPayment, cardPayment;
@Inject
public PaymentProvider(CardPayment cardPayment, CashPayment cashPayment) {
this.cardPayment = cardPayment;
this.cashPayment = cashPayment;
}
public Payment getPaymentByMethod(PaymentMethod method) {
switch (method) {
case CARD:
return cardPayment;
case CASH:
return cashPayment;
default:
throw new IllegalArgumentException("Unkown payment method: " + method);
}
}
}
public enum PaymentMethod { CASH, CARD }
public class Order {
private final PaymentProvider paymentProvider;
@Inject
public Order(PaymentProvider paymentProvider) {
this.paymentProvider = paymentProvider;
}
public void finishOrder(PaymentMethod method) {
paymentProvider.getPaymentByMethod(method).pay();
}
}
Still for your own practise: The Payment stuff. You do not need any Guice code in there. The rest is done by Guice automagically. If you start using interfaces you would have start to use bindings as described here: http://code.google.com/p/google-guice/wiki/GettingStarted. But if you do not have any interfaces, the construction is done without any configuration. The @Inject annotations are enough.
Of course this is just an example design. But it shows how easy it is to setup a nicely decoupled Java app with Guice.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With