One of the most useful things about object oriented programming is polymorphism. In fact, it makes our software have different behaviors without the need to explicit it in the code.
For instance, you are writing a code to make a credit card transaction. If it is a VISA transaction, you should execute one specific action, if it is a MASTERCARD transaction, you should execute another specific action, so, the first thing you think about doing is:
public void pay(Transaction transaction) throws UnsupportedCardNetworkException {
if("visa".equals(transaction.getType())){
//pay with visa
return;
}
if("mastercard".equals(transaction.getType())){
//pay with mastercard
return;
}
throw new UnsupportedCardNetworkException();
}
That's not a good design, is it? Probably we need to write a lot of code for each card network, besides, if I want to add support to another card network, I will need another if. Someday this code is going to be a little demon.
So, what should I do? Well, in oriented programming, there is something what people call "polymorphism", and what they say that the same method could have several behaviors. Well, that's just what we need for this situation. So, how can we begin?
Well, we are going to have an interface called Payment, having a method, of course, called "pay":
public interface Payment {
void pay(Transaction transaction);
}
That's pretty simple, actually. So, as we have two credit card networks to implement, we are going to have two more classes, one implementing visa, another implementing mastercard:
public class VisaPayment implements Payment {
@Override
public void pay(Transaction transaction) {
//paying with visa
}
}
public class MasterCardPayment implements Payment {
@Override
public void pay(Transaction transaction) {
//paying with mastercard
}
}
So, when I need to pay using a visa card, I just do:
Payment payment = new VisaPayment();
payment.pay(transaction);
And, when I need to pay using mastercard:
Payment payment = new MasterCardPayment();
payment.pay(transaction);
That solves our first "pay" method, which was going to be a big mess.
But, what if I want to do something dynamically? For instance, I don't want to use ifs in my code to choose which instance of a Payment I am going to use, I just want to tell the code to give me an instance based on some variable, like:
Payment payment = payments.get("visa");
payment.pay(transaction)
Well, I said something dynamic, so:
Payment payment = payments.get(transaction.getType());
payment.pay(transaction);
That's pretty good, actually! But how can we do that? Well, the anwser is short and simple:
public class PaymentFactory {
private static final Map payments;
private PaymentFactory(){}
static {
payments = new HashMap();
payments.put("visa", new VisaPayment());
payments.put("mastercard", new MasterCardPayment());
}
public static Payment get(String type){
return payments.get(type);
}
}
So, when I want to get a Payment instance:
Payment payment = PaymentFactory.get(transaction.getType());
Quite simple, isn't it? Well, you should be wondering "but, why is there a CDI in the title of this article?", well, now you are going to know why and see how this stuff gets interesting in here.
CDI allows us to create several beans of a super type and choose a instance by a qualifier, so, here is what we are going to do: Create a qualifier called @Network with a field which represents the name of the network. After this, we will annotate those two Payment implementations, just setting the network name in each one.
@Retention(RetentionPolicy.RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER, CONSTRUCTOR})
@Qualifier
public @interface Network {
String value();
}
@Network("mastercard")
public class MasterCardPayment implements Payment {
@Override
public void pay(Transaction transaction) {
//paying with mastercard
}
}
@Network("visa")
public class VisaPayment implements Payment {
@Override
public void pay(Transaction transaction) {
//paying with visa
}
}
We can't select an instance just by the string value, so, we need to create a class whichs extends from AnnotationLiteral and implements our Network annotation.
public class NetworkAnnotationLiteral extends AnnotationLiteral<Network> implements Network{
private String value;
private NetworkAnnotationLiteral(String value) {
this.value = value;
}
@Override
public String value() {
return value;
}
public static NetworkAnnotationLiteral network(String value){
return new NetworkAnnotationLiteral(value);
}
}
If I want an instance of NetworkAnnotationLiteral, I just call:
NetworkAnnotationLiteral.network("type");
But, how can we select our instance based on a transaction type now? Well:
@Inject @Any
private Instance<Payment> payments;
@Test
public void payWithVisa(){
Transaction transaction = new Transaction();
transaction.setType("visa");
Payment payment = payments.select(NetworkAnnotationLiteral.network(transaction.getType())).get();
payment.pay(transaction);
}
@Test
public void payWithMaster(){
Transaction transaction = new Transaction();
transaction.setType("mastercard");
Payment payment = payments.select(NetworkAnnotationLiteral.network(transaction.getType())).get();
payment.pay(transaction);
}
When we inject the object javax.enterprise.inject.Instance<Payment> with the qualifier @Any, every instance of Payment will be available for you to choose and use. The nicest thing is: now CDI will control the creation of your objects, you don't need to write your own factory, you can also inject resources into your Payment subtypes and choose any CDI scope you want.
That's not a new thing in CDI, but, most people still don't know how powerful CDI is.