Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

An elegant solution to finding which interface implementation to use based on a string provided in runtime

While refactoring some code, I found that we should be using some polymorphism in several places rather than having to have a bunch of if/else blocks all over the place.

While the object oriented classes were very easy to make, the issue comes when we have to decide what implementation to use. There seems to be many solutions but I wanted to see if there were any more elegant or "best practice" ways of doing this.

So essentially, say for the sake of example, I am going to choose a Credit Card and for each credit card type, I have an implementation. Therefore I have a class structure like this:

  • CreditCard (abstract)
    • Visa
    • MasterCard
    • Amex

Each of the child classes would extend the CreditCard super class.

Now in the web application, I would pass down a String that represents the card type the user has chosen. Now I need to route that to the actual implementing class itself. This is where the plethora of choices comes into play.

Please do let me know if there are any better choices or if I'm stuck with these..

Factory:

        @Autowired @Qualifier("visa") private CreditCard visa;
        @Autowired @Qualifier("mastercard") private CreditCard mastercard;
        @Autowired @Qualifier("amex") private CreditCard amex;

        public CreditCard getCreditCard(String input) {
        {
            if ("visa".equals(input)) {
                    return visa;
            } else if ("mastercard".equals(input)) {
                    return mastercard;
            } else if ("amex".equals(input)) {
                    return amex;
            }

            return null;
        }

Map:

        @Autowired HashMap<String, CreditCard> creditCardImpls;

        public CreditCard getCreditCard(String input) {
            return creditCardImpls.get(input);
        }

ApplicationContext getBean:

        @Autowired private ApplicationContext applicationContext;

        public CreditCard getCreditCard(String input) {
            return applicationContext.getBean(input);
        }

The issue I am seeing here is that with the factory, I'm going to have to autowire in potentially several different fields if we're going to add many more credit card types in the future. Then, the issue with the Map is that we're not using Spring to grab the bean. For t he getBean from the ApplicationContext, we're not following the IoC that Spring provides.

What would be the best solution or best practice for this problem?

like image 959
user3610318 Avatar asked May 07 '14 01:05

user3610318


1 Answers

First some comment on your solutions, then my own proposal.

  1. This violates the Open-Closed Principle and Dependency Inversion Principle. Not only the factory needs to explicitly know about all the beans separately and their corresponding credit card type (input) mappings, but also every time you want to introduce a new credit card type, you need to modify the factory class code.
  2. This is by far the best of your solutions, IMO. It discovers the beans dynamically and depends on the CreditCard abstraction. On the contrary to your comment that "it's not using Spring to determine which bean to use", the map has bean IDs as keys and is autowired by Spring, so we are in fact using Spring extensively here. However, since the map keys are bean IDs, that mixes the business logic (credit card type - input) with the technical implementation (Spring bean ID). What if you want to implement a separate bean to handle "mastercard platinum" credit card type? For technical reasons, you cannot have such a bean ID (due to the space). Or at some point you'll want that a certain bean can handle multiple card types. Then, you'd need to implement another translation map between the business identifiers and bean IDs. See my solution below for an enhancement.
  3. As far as possible, you should be avoiding coupling your code with the framework code (Spring or any other). As you commented, we should let Spring handle the DI for us, so that the factory class has the dependencies injected and does not have to ask for them explicitly.

My suggestion would be to separate the business key from the bean ID. So we need the CreditCard to be able to say which credit card type it can handle.

abstract class CreditCard {
    public abstract String getType();
}

Then each subclass returns its own type.

Or

abstract class CreditCard {
    private String type;

    protected CreditCard(String type) {
        this.type = type;
    }

    public String getType() {
        return type;
    }
}

and each subclass calls the super(<the subclass type>) constructor within their own constructor.

Then, in the factory, you can implement is as such.

@Autowire private Collection<CreditCard> creditCards;

private Map<String, CreditCard> creditCardsMap = new HashMap<>();

@PostCostruct
public void mapCreditCards() {
    for (CreditCard creditCard : creditCards) {
        creditCardsMap.put(creditCard.getType(), creditCard);
    }
}

public CreditCard getCreditCard(String type) {
    return creditCardsMap.get(type);
}

The @PostConstruct method will run after the creditCards (all the beans with the type CreditCard) have been autowired, thus allowing for both dynamic discovery and mapping under a business key rather than a technical key.

like image 103
Adam Michalik Avatar answered Oct 27 '22 20:10

Adam Michalik