简体   繁体   中英

Use one implementation of an interface depending on used generic in implementation (applied type)

I have two interfaces. One interface contains information, and the second interface is supposed to use the first interface. The second interface have one generic(s) that has to be a implementation of the first interface.

I want to automatically use the implementation of the second interface depending on what implementation of the first interface I receive.

Let me show the interfaces. (I changed domain and simplified it, but you get the basic idea.)

//This contains information needed to publish some information
//elsewhere, on a specific channel (MQ, Facebook, and so on)
public interface PubInfo {

    String getUserName();
    String getPassword();
    String getUrl();

    Map<String, String> getPublishingSettings();
} 



//Implementation of this interface should be for one implementation 
//PubInfo
public interface Publisher<T extends PubInfo> {
    void publish(T pubInfo, String message);
}

Lets assume I would have these implementations of PubInfo ...

public class FacebookPubInfo implements PubInfo {
    // ...
}

.

public class TwitterPubInfo implements PubInfo {
    // ...
}

...and these of Publisher

@Component
public class FacebookPublisher implements Publisher<FacebookPubInfo> {

    @Override
    public void publish(FacebookPubInfo pubInfo, String message) {
        // ... do something
    }
}

.

@Component
public class TwitterPublisher implements Publisher<TwitterPubInfo> {
    // ...
}    

You get the basic idea, two interfaces with two implementations each.

To the question, finally

Now I'll come to the tricky part for me, and that is that I want to be able to automatically use TwitterPublisher when my service gets a TwitterPubInfo.

I can do that with manual mapping, as you see in the example below, but I can't help to think that it would exist a way to do this more automatically, and not depending upon manual mapping. I use spring, and I think that in there, somewhere it would exist a tool to help me with this, or maybe some other utility class, but I can't find anything.

@Service
public class PublishingService {

    private Map<Class, Publisher> publishers = new HashMap<Class, Publisher>();

    public PublishingService() {

        // I want to avoid manual mapping like this
        // This map would probably be injected, but 
        // it would still be manually mapped. Injection
        // would just move the problem of keeping a 
        // map up to date.
        publishers.put(FacebookPubInfo.class, new FacebookPublisher());
        publishers.put(TwitterPubInfo.class, new TwitterPublisher());
    }

    public void publish(PubInfo info, String message) {

        // I want to be able to automatically get the correct
        // implementation of Publisher

        Publisher p = publishers.get(info.getClass());
        p.publish(info, message);

    }

}

I could at least populate publishers in PublishingService with reflections, right?

Do I need to do it myself, or is there any help somewhere else with this?

Or, maybe you think the approach is wrong, and that there exists a smarter way to accomplish what I need to do here, feel free to say that and tell me your superior way :p of doing things (really, I appreciate it).

edit 1 start

When writing custom eventhandlers in spring it finds the correct implementation, and that is where I got my inspiration to this question.

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/beans.html#context-functionality-events

This is from that page:

public class BlackListNotifier implements ApplicationListener<BlackListEvent> {

    // ...

    public void onApplicationEvent(BlackListEvent event) {
        // as you can see spring solves this, somehow, 
        // and I would like to be able to something similar
    }

}

Can I get the same functionality, somehow?

end edit 1

Are your Publisher implementations Spring beans?

In that case, you can get all of those using:

Map<String, Publisher> pubs = context.getBeansOfType(Publisher.class);

And then you can ask each Publisher if it will accept the PubInfo you received (that would mean adding a new method to Publisher so that each publisher can decide which PubInfo it can process).

This solution would avoid the manual mapping, and each Publisher would encapsulate the information related to what it can process.

You can also use an annotation in each Publisher class, then get all beans that have that annotation (and the annotation can indicate the particular class that Publisher can process). It's a similar approach but perhaps you'd find it nicer with annotations.

What you would like to do is this below... but that does not work as far as I know. The solution I suggest comes close to that.

// does not work...
context.getBeansOfType(Publisher<pubInfo.getClass()>.class);

Spring actually autowires beans of an abstract type to a map, with the key being the bean name and the value being the actual bean instance:

@Service
public class PublishingService {

    @Autowired
    private Map<String, Publisher> publishers;

    public void publish(PubInfo info, String message) {
        String beanName = info.getClass().getSimpleName();
        Publisher p = publishers.get(beanName);
        p.publish(info, message);
    }
}

For this to work, you'd need to set the bean name of every publisher to match the simple class name of its corresponding PubInfo concrete implementation.

One way to do it is by means of Spring's @Component annotation:

@Component("FacebookPubInfo")
public class FacebookPublisher implements Publisher<FacebookPubInfo> {

    @Override
    public void publish(FacebookPubInfo pubInfo, String message) {
        // ... do something
    }
}

Then you'd only need to make Spring scan this class and follow the same approach with the TwitterPubInfo class.

Note: If you're using XML configuration, you could use the same approach. Instead of using @Component and scanning your classes, just set every publisher's bean name explicitly in the XML.

If you are interested, I have created a Gist for this.

Solution

Thanks to @eugenioy and @Federico Peralta Schaffne answers I could get the endresult I wanted. I also found an interesting commet from @Jonathan in this question ( Get generic type of class at runtime ) . In that comment TypeTools were mentioned, and that was the last piece I needed to put this togheter.

I ended up writing a small component that automatically makes the mapping, and can return the implementing class.

If you know if there exists such component in a library, please let me know . That was actually what I was searching for in the first place (I'll mark my answer as the correct one, but if you find such a component in a maintained library in a public repo, I gladly make your answer the correct one, it just have to be able to do what my component can do).

To get TypeTools I added this to my pom:

    <dependency>
        <groupId>net.jodah</groupId>
        <artifactId>typetools</artifactId>
        <version>0.4.3</version>
    </dependency>

Now the implementaion of PublishingService looks like this:

@Service
public class PublishingService {

    //This bean is manually defined in application context. If spring could
    //understand how to do this from the Generic I specified below (Publisher)
    //it would be close to perfect. As I understand it, this information is
    //lost in runtime.

    @Autowired
    private ImplementationResolver<Publisher> publisherImplementationResolver;

    public void publish(PubInfo info, String message) {

        Publisher p = publisherImplementationResolver.getImplementaion(info);
        p.publish(info, message);

    }

}

The bean publisherImplementationResolver is constructed by the SpringContext

@SpringBootApplication
public class Main  {

    @Bean
    public ImplementationResolver publisherImplementationResolver() {
        //If spring could figure out what class to use, It would be even better
        //but now I don't see any way to do that, and I manually create this
        //bean
        return new ImplementationResolver<Publisher>(Publisher.class);
    }

    public static void main(String[] args) throws Exception {
        SpringApplication.run(Main.class, args);
    }
}

The class ImplementationResolver gets all beans that implements Publisher and uses TypeTools to map the specified generic (or maybe applied type is more correct to say).

/**
 * Created on 2015-10-25.
 */
public class ImplementationResolver<T> implements ApplicationContextAware {

    private Class<T> toBeImplemented;


    private Map<String, T> implementations;


    private Map<Class, T> implemenationMapper;


    public ImplementationResolver(Class<T> toBeImplemented) {
        this.toBeImplemented = toBeImplemented;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        implementations = applicationContext.getBeansOfType(toBeImplemented);
    }

    @PostConstruct
    public void intializeMapper() {
        implemenationMapper = new HashMap<Class, T>();
        for (String beanName : implementations.keySet()) {
            T impl = implementations.get(beanName);

            Class<?>[] typeArgs = TypeResolver.resolveRawArguments(toBeImplemented, impl.getClass());

            implemenationMapper.put(typeArgs[0], impl);
        }
    }

    public T getImplementaion(Object whatImplementationIsDoneForMe) {
        return implemenationMapper.get(whatImplementationIsDoneForMe.getClass());
    }
}

To test that this works as intended I have this class:

@Component
public class ImplementationDemoResolver  implements CommandLineRunner {

    @Autowired
    PublishingService publishingService;

    @Override
    public void run(String... strings) throws Exception {
        FacebookPubInfo fb = new FacebookPubInfo();
        TwitterPubInfo tw = new TwitterPubInfo();

        PubInfo fb2 = new FacebookPubInfo();
        PubInfo tw2 = new TwitterPubInfo();

        publishingService.publish(fb, "I think I am a interesting person, doing interesting things, look at this food!");
        publishingService.publish(tw, "I am eating interesting food. Look! #foodpicture");
        publishingService.publish(fb2, "Wasted Penguinz is the sh17");
        publishingService.publish(tw2, "Join now! #wpArmy");
    }
}

When I run the program I get this result ( FacebookPublisher and TwitterPublisher writes FACEBOOK and TWITTER respectevly):

FacebookPubInfo will provide information on how to publish on FACEBOOK. Message: I think I am a interesting person, doing interesting things, look at this food!
TwitterPubInfo will provide information on how to publish on TWITTER. Message: I am eating interesting food. Look! #foodpicture
FacebookPubInfo will provide information on how to publish on FACEBOOK. Message: Wasted Penguinz is the sh17
TwitterPubInfo will provide information on how to publish on TWITTER. Message: Join now! #wpArmy

Motivation

Why choose this instead of the solution provided by Federico Peralta Schaffne?

This solution does not need any extra information in the implementing classes. On the other hand it requires a special setup, but I think it is worth while. It is also possible to use the ImplementationResolver in other places, as it is a separate component. It also works automatically once it is in place.

If I would trust my collegues I could go with the solution provided by Federico Peralta Schaffne (not all of them wants to understand Spring and tdd), it feels a bit more lightweight then mine solution. Refactoring might cause some problems if the name of the bean is in a string (but Intellij would find it, maybe other ide:s as well).

Improvments

Now the component is limited to interfaces with only one generic, but with a signature like in the code below it could cover more use cases. It would still be tricky with something like this AdvancedPublisher<Map<T>, G<T>> , so it will still not be perfect, but better. Since I don't need that, I have note implemented it. It could be done with two layers of different collections, so if you would need that, it would not be to complicated to do.

public class ImplementationResolver<T> implements ApplicationContextAware {

    // ...

    public ImplementationResolver(Class<T> ... toBeImplemented) {
        this.toBeImplemented = toBeImplemented;
    }

    // ...

    public T getImplementaion(Object ... whatImplementationIsDoneForMe) {
        // .... implementation
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM