Marco Zaccara
Marco Zaccara

Reputation: 31

Implements a Factory Pattern in java with using Generics

I need help with Java Generics. My model is: I have some classes that extends a Dto (Data Transfer Object) and some classes that extends Entity (The model of my object to DB). I have

interface Mapper<D extends Dto, E extends Entity>{
//Convert a Entity to Dto.
D toDto(E entity);

And I have some classes that implements this interface (i.e PersonMapper, BookMapper and so far and so on).

@Component    
public class PersonMapper implements Mapper<PersonDto, PersonEntity> {
    //implementation
    }

@Component    
public class BookMapper implements Mapper<BookDto, BookEntity> {
    //implementation
    }

What I want to do is to use Factory Pattern in order to select at runtime my Mapper, that depends from a String that I pass in input.

@Autowired
private PersonMapper personMapper;
@Autowired
private BookMapper bookMapper;    

public <D extends Dto, E extends Entity> Mapper<D, E> selectMapper(String entity){
      if ("Person".equalsIgnoreCase(entity))
         return personMapper;
      if("Book".equalsIgnoreCase(entity))
        return bookMapper;
      ...
    }

With this situation I have the following compile error:

Type mismatch: cannot convert from PersonMapper to Mapper<D,E>

My solutions:

1)

return (Mapper<D, E>) personMapper;

but I have a Warning:

Type Safety: `Unchecked class from personMapper to Mapper<D,H>`

2)

Using WildCard and castingb

public Mapper<Dto, Entity> selectMapper(String entity){
          Mapper<? extends Dto, ? extends Entity> toReturn = null;
          if ("Person".equalsIgnoreCase(entity))
             toReturn = personMapper;
          else if("Book".equalsIgnoreCase(entity))
            toReturn = bookMapper;
          ...
          return (Mapper<Dto, Entity>) toReturn;
        }

But in this case but I have another time a Warning:

Type safety: Unchecked cast from Mapper<capture#29-of ? extends Dto,capture#30-of ? extends Entity> to Mapper<Dto,Entity>

It works but it doesn't seems to be a clean solution

3) Using wildcard as return type:

public Mapper<? extends Dto, ? extends HistoryEntity> selectMapper(String entity)

but you know, using wildcard as return type is not recommended at all and also doesn't help me because I would like to use this mapper and call mapper.toDto ensuring that the return type is an something that extends Dto.

====================================================================

I don't explain why If I write a class constructor like that

public Service<D extends Dto, E extends Entity>{  
   public Service(Mapper<D,E> mapper){ 
      this.mapper = mapper; 
   } 
} 

and than I inject (for example) bookMapper it works.

If, instead, the Mapper<D,E> is in return type I cannot do such a kind of operation.

====================================================================

The help that I ask to you is:

how can I write a solution using clean code principles (avoiding compile warnings, sonarlint issue etc.) in order to implement this kind of logic?

Thank you very much, I appreciate a lot if you dedicate a little bit of your time helping me to solve my problem.

Upvotes: 3

Views: 971

Answers (2)

william0754
william0754

Reputation: 44

Factory Pattern is pattern that assemble or create something by factory methods, in you case what you need is just to get corresponding mapper by name, so there is a simple way to do that since the mapper beans are autowired, adding String getName() to Mapper interface then implements it for earch implementation, e.g. in BookMapper

    @Override
    public String getName() { return "Book"; }

use mapper name as key and mapper bean as value to store mapper beans in a map, then you can retrieve it by its name:

@Service
public class SimpleService {
    private BookMapper bookMapper;
    private PersonMapper personMapper;
    private Map<String, Mapper<? extends DTO, ? extends Entity>> mappers = new HashMap<>();

    public SimpleService(BookMapper bookMapper, PersonMapper personMapper) {
        this.bookMapper = bookMapper;
        this.personMapper = personMapper;
        mappers.put(bookMapper.getName(), bookMapper);
        mappers.put(personMapper.getName(), personMapper);
    }

    public Mapper<? extends DTO, ? extends Entity> getMapperByName(String mapperName) {
        return mappers.get(mapperName);
    }
}

and you can cast it to corresponding mapper without warning.

        PersonMapper p = (PersonMapper) simpleService.getMapperByName("Person");

or you can put different mapper in their service and use the service to handle you biz likes codes below, after all, you need specified mappers to do specified operations:

     if(personThings){
         personService.doSomeThing();
      }
     if(bookThings){
         bookService.doSomething();
     }

Upvotes: 0

rzwitserloot
rzwitserloot

Reputation: 102902

Those vars (D and E) about the caller and not about your code. The D and E are decided by the caller, so there is absolutely no way to guarantee that PersonDTO fits.

Make that Mapper<? extends DTO, ? extends Entity> (and no variables), and given that those are already the lower bounds, just Mapper<?, ?> - that'll work, you can write your return statements without any casts and without compiler errors or warnings.

Of course, it means the caller has a mostly useless type.

Generics are entirely 'compile time / write time' based. The JVM (java.exe) has no idea what generics are, and in fact most of them don't survive the compilation process. The one and only purpose of generics is to make the compiler flag incorrect code and avoid some casting, that is all.

The nature of turning that string into a Mapper is entirely runtime.

Ergo, if Mapper<?, ?> isn't sufficient, what you want isn't possible. You'd need to write compile/write-time checkable stuff, so the moment you use a String, it's impossible. For example, a method getPersonMapper() can of course return a Mapper<PersonDTO, PersonEntity>, no problem.

More generally (heh) it sounds like you're badly reinventing various wheels here. Look at tutorials of JDBI, JOOQ, and Hibernate to get some ideas about how java code is commonly written to interact with databases.

Upvotes: 2

Related Questions