How to implement factory pattern with generics in

2019-03-19 06:21发布

问题:

I have a generic interface Handler

public interface Handler<T> {
  void handle(T obj);
}

I can have n implementations of this interface. Let's say I have following 2 implementations for now. One which handles String objects and another handles Date

public class StringHandler implements Handler<String> {
  @Override
  public void handle(String str) {
    System.out.println(str);
  }
}

public class DateHandler implements Handler<Date> {
  @Override
  public void handle(Date date) {
    System.out.println(date);
  }
}

I want to write a factory which will return handler instances based on the class type. Something like this :

class HandlerFactory {
  public <T> Handler<T> getHandler(Class<T> clazz) {
    if (clazz == String.class) return new StringHandler();
    if (clazz == Date.class) return new DateHandler();
  }
}

I get following error in this factory :

Type mismatch: cannot convert from StringHandler to Handler<T>

How to fix this?

回答1:

SIMPLE SOLUTION

You could save your mappings Class<T> -> Handler<T> in a Map. Something like:

Map<Class<T>, Handler<T>> registry = new HashMap<>();

public void registerHandler(Class<T> dataType, Class<? extends Handler> handlerType) {
    registry.put(dataType, handlerType);
}

public <T> Handler<T> getHandler(Class<T> clazz) {
  return registry.get(clazz).newInstance();
}

In some place, initialize handlers (could be in the factory itself):

factory.registerHandler(String.class, StringHandler.class);
factory.registerHandler(Date.class, DateHandler.class);

And in another place, you create and use them:

Handler<String> stringhandler = factory.getHandler(String.class);
Handler<Date> dateHandler = factory.getHandler(Date.class);

MORE COMPLEX SOLUTION

You can "scan" classes using reflection and, instead of register manually the mappings Class<T> -> Handler<T>, do it using reflection.

for (Class<? extends Handler> handlerType : getHandlerClasses()) {
    Type[] implementedInterfaces = handlerType.getGenericInterfaces();
    ParameterizedType eventHandlerInterface = (ParameterizedType) implementedInterfaces[0];
    Type[] types = eventHandlerInterface.getActualTypeArguments();
    Class dataType = (Class) types[0]; // <--String or Date, in your case
    factory.registerHandler(dataType, handlerType);
}

Then, you create and use them like above:

Handler<String> stringhandler = factory.getHandler(String.class);
Handler<Date> dateHandler = factory.getHandler(Date.class);

To implement getHandlerClasses(), look at this to scan all classes in your jar. For each class, you have to check if it is a Handler:

if (Handler.class.isAssignableFrom(scanningClazz) //implements Handler
    && scanningClazz.getName() != Handler.class.getName()) //it is not Handler.class itself
{
        //is a handler!
}

Hope it helps!



回答2:

Your problem is that the compiler cannot make the leap to the fact thet the type of the result is correct.

To help the compiler you can make the factory delegate the construction. Although this looks strange and unwieldly it does manage to properly maintain type safety without sacrifices such as casting or using ? or raw types.

public interface Handler<T> {

    void handle(T obj);
}

public static class StringHandler implements Handler<String> {

    @Override
    public void handle(String str) {
        System.out.println(str);
    }
}

public static class DateHandler implements Handler<Date> {

    @Override
    public void handle(Date date) {
        System.out.println(date);
    }
}

static class HandlerFactory {

    enum ValidHandler {

        String {
                    @Override
                    Handler<String> make() {
                        return new StringHandler();
                    }
                },
        Date {
                    @Override
                    Handler<Date> make() {
                        return new DateHandler();
                    }
                };

        abstract <T> Handler<T> make();
    }

    public <T> Handler<T> getHandler(Class<T> clazz) {
        if (clazz == String.class) {
            return ValidHandler.String.make();
        }
        if (clazz == Date.class) {
            return ValidHandler.Date.make();
        }
        return null;
    }
}

public void test() {
    HandlerFactory factory = new HandlerFactory();
    Handler<String> stringHandler = factory.getHandler(String.class);
    Handler<Date> dateHandler = factory.getHandler(Date.class);
}


回答3:

You can use something like:

class HandlerFactory {
  public <T> Handler<T> getHandler(Class<T> clazz) {
    if (clazz.equals(String.class)) return (Handler<T>) new StringHandler();
    if (clazz.equals(Date.class)) return (Handler<T>) new DateHandler();

    return null;
  }
}

T is generic and the compiler can't map that at compile time. Also it is safer to use .equals instead of ==.



回答4:

The whole point of using a generic type is to share the implementation. If the n implementation of your Handler interface are so different that they can't be shared, then I don't think there is any reason to use define that generic interface at the first place. You'd rather just have StringHandler and DateHandler as top level classes.

On the other hand, if the implementation can be shared, as is the case of your example, then the factory works naturally:

public class Main {
    static public interface Handler<T> {
      void handle(T obj);
    }

    static public class PrintHandler<T> implements Handler<T> {
      @Override
      public void handle(T obj) {
        System.out.println(obj);
      }
    }

    static class HandlerFactory {
      public static <T> Handler<T> getHandler() {
        return new PrintHandler<T>();
      }
    }

    public static void main(String[] args) {
      Handler<String> stringHandler = HandlerFactory.getHandler();
      Handler<Date> dateHandler = HandlerFactory.getHandler();

      stringHandler.handle("TEST");
      dateHandler.handle(new Date());
  }
}


回答5:

Basically you can do:

 public Handler getHandler( Class clazz ){
        if( clazz == String.class ) return new StringHandler();
        if( clazz == Date.class ) return new DateHandler();

        return null;
    }

    public static void main( String[] args ){
        HandlerFactory handlerFactory = new HandlerFactory();
        StringHandler handler = ( StringHandler )handlerFactory.getHandler( String.class );
        handler.handle( "TEST" );
        DateHandler handler2 = ( DateHandler )handlerFactory.getHandler( Date.class );
        handler2.handle( new Date() );
    }

Output:

TEST
Tue Dec 15 15:31:00 CET 2015

But instead writing two different methods to get handlers separately always is a better way.



回答6:

I edited your code and allowed Eclipse to "fix" the errors and it came up with this.

public Handler<?> getHandler(Class<?> clazz) {
    if (clazz == String.class)
        return new StringHandler();
    if (clazz == Date.class)
        return new DateHandler();

    return null;
}


回答7:

Yout HandlerFactory don't know about T. Use your factory like below-

public class HandlerFactory {
    public Handler<?> getHandler(Class<?> clazz) {
      if (clazz == String.class) {
          return new StringHandler();
      }
      if (clazz == Date.class) {
          return new DateHandler();
      }
      return null;
    }
}