Replace hardcoded class implementations

2019-09-03 11:46发布

问题:

While working on a new project a came across with the following familiar case.

private static Hashtable<String, Class<? extends MyExample>> MyExampleClassCollection = new Hashtable<String, Class<? extends MyExample>>();

static {
    MyExampleClassCollection.put("example1", MyExampleImplementation1.class);
    MyExampleClassCollection.put("example2", MyExampleImplementation2.class);
    MyExampleClassCollection.put("example3", MyExampleImplementation3.class);
}

public static MyExample getMyExample(String myExampleType){

    Class<? extends MyExample> templateClass = MyExampleClassCollection.get(myExampleType);

    try {
        Constructor ctor = templateClass.getConstructor(Connection.class);
        return (MyExample)ctor.newInstance();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }

It's the case where you are trying to create a more generic code and you end up hardcode the different implementations of your generic class. Having the down side that you have to create a new entry on the Hashtable for every new implementation.

A solution I saw on a previous project I worked on, was to store the several names of the concrete classes in the DB. Having a field with the name of the concrete class that is going to be used. And create it again with reflection, for example MyExampleImplementation1. Gain that you don't use a HashTable. This solution is identical, the only difference is I retrieve the myExampleType from the DB.

Is there a more elegant way? I would prefer to use a configuration file. I thought I could use dependency injection (I'm not very experienced with this yet), but is this a good case for that?

Is there a way to integrate this functionality with my application server ?

回答1:

Summary : You don't need to enumerate and store components that are required across an application, by yourself. The container will do that for you; all you need to do is look it up whenever you need the components.


cdi or jndi ought to do here. What you're going to wind up with is a variation of the Service Locator pattern. This solution presumes you're not looking to rewrite a lot of code or any of the clients of getMyExample (if you are, there are better ways to do it).

JNDI

A JNDI approach here is quick, no-nonsense, but also (IMO) inelegant and ugly. It's simple:

  1. Declare an entry per class in your web.xml (if you're in a web application)

    <env-entry>
       <env-entry-name>example1</env-entry-name>
       <env-entry-type>java.lang.Class</env-entry-type>
       <env-entry-value>my.example.MyExampleImplementation1</env-entry-value>
    </env-entry>
    
  2. Look it up whenever you need it

    You have at least 2 options here - using the @Resource annotation or using old-fashioned InitialContext or SessionContext JNDI lookups (SessionContext is faster). Since your objective here is to avoid hardcoding, the context lookup is the way to go:

    @Resource
    private SessionContext sessionContext;
    
    public static MyExample getMyExample(String myExampleType){
    
        Class<? extends MyExample> theClass =  (Class<? extends MyExample>) sessionContext.lookup(myExampleType);
        //instantiate
    }
    

It's pure configuration, but again, not very elegant. Also, support for classes as <env-entry> started with JavaEE 6.


CDI

One of the many benefits of CDI is the beanification that it provides by default to many artefacts in the Java EE space. Let's start here

  • You don't want to hard-code the types that you're going to need to instantiate
  • You're working towards cleaner code

A combination of the following CDI constructs should sort you out:

  • CDI Bean Manager
  • Naming

To start, create a beans.xml file in your WEB-INF or META-INF folders. This is a prerequisite to enabling CDI in your web application.

Naming:

After enabling CDI in your web application above, almost all your classes in your application are available to the CDI engine to inspect, i.e. they become beans, based on some eligibility rules. You now want to be able to refer to your implementation classes by names that you as the author of the class specify. Use the CDI @Named on the beans.

@Named("example1")
public class MyExampleImplementation1

In any part of your application, you can now refer to that class, by that name.

BeanManager

The BeanManager is the overlord of the beans in the CDI context - it's your gateway into the all the beans and other inner workings of the CDI engine. You'll use this to retrieve an instance of the desired class by name

@Inject BeanManager manager; //inject the bean manager into your class


public static MyExample getMyExample(String myExampleType){
    Set<Bean<?>> beans = manager.getBeans("example1"); //retrieve the bean by name. This returns a set, but you're ideally interested in only one bean
    Bean bean = beans.iterator().next(); //get the first (and ideally the only bean in the set)
    CreationalContext ctx = manager.createCreationalContext(bean); //CDI stipulation: there has to be a context associated with everything.
    MyExample theExample = bean.create(ctx); //create an instance of the bean and return

    return theExample;

}

With this approach, there's no need to continue updating a configuration file everytime you add support for a new MyExample implementation

Tip : Cache the objects that are created to reduce lookup and instantiation overhead in both approaches



回答2:

Just a few small changes to make it work.

Assuming that the MyExampleImplementation1 implements an interface MyExample, you need to pass this as parameter rather than hard-code it in the manager.getBeans().

Also you will need to cast the result of the instatiation.

Last thing, the injection of the BeanManager isn't static, so the getMyExample method is not allowed to be static, either.

@Inject BeanManager manager; //inject the bean manager into your class


public MyExample getMyExample(String myExampleType){
    Set<Bean<?>> beans = manager.getBeans(myExampleType); //retrieve the bean by name. This returns a set, but you're ideally interested in only one bean
    Bean bean = beans.iterator().next(); //get the first (and ideally the only bean in the set)
    CreationalContext ctx = manager.createCreationalContext(bean); //CDI stipulation: there has to be a context associated with everything.
    MyExample theExample (MyExample) = bean.create(ctx); //create an instance of the bean and return

    return theExample;

}