0

I am working on a REST API where I have an interface that defines a list of methods which are implemented by 4 different classes, with the possibility of adding many more in the future.

When I receive an HTTP request from the client there is some information included in the URL which will determine which implementation needs to be used.

Within my controller, I would like to have the end-point method contain a switch statement that checks the URL path variable and then uses the appropriate implementation.

I know that I can define and inject the concrete implementations into the controller and then insert which one I would like to use in each particular case in the switch statement, but this doesn't seem very elegant or scalable for 2 reasons:

  1. I now have to instantiate all of the services, even though I only need to use one.

  2. The code seems like it could be much leaner since I am literally calling the same method that is defined in the interface with the same parameters and while in the example it is not really an issue, but in the case that the list of implementations grows ... so does the number of cases and redundant code.

Is there a better solution to solve this type of situation? I am using SpringBoot 2 and JDK 10, ideally, I'd like to implement the most modern solution.

My Current Approach

@RequestMapping(Requests.MY_BASE_API_URL)
public class MyController {

    //== FIELDS ==
    private final ConcreteServiceImpl1 concreteService1;
    private final ConcreteServiceImpl2 concreteService2;
    private final ConcreteServiceImpl3 concreteService3;

    //== CONSTRUCTORS ==
    @Autowired
    public MyController(ConcreteServiceImpl1 concreteService1, ConcreteServiceImpl2 concreteService2,
                              ConcreteServiceImpl3 concreteService3){
      this.concreteService1 = concreteService1;
      this.concreteService2 = concreteService2;
      this.concreteService3 = concreteService3;
    }


    //== REQUEST MAPPINGS ==
    @GetMapping(Requests.SPECIFIC_REQUEST)
    public ResponseEntity<?> handleSpecificRequest(@PathVariable String source,
                                                       @RequestParam String start,
                                                       @RequestParam String end){

        source = source.toLowerCase();
        if(MyConstants.SOURCES.contains(source)){
            switch(source){
                case("value1"):
                    concreteService1.doSomething(start, end);
                    break;
                case("value2"):
                    concreteService2.doSomething(start, end);
                    break;
                case("value3"):
                    concreteService3.doSomething(start, end);
                    break;
            }
        }else{
            //An invalid source path variable was recieved
        }

        //Return something after additional processing
        return null;
    }
}
4
  • I think, If source possibilities are few you can have separate rest mapping api's for each source. or if there are more source possibilities, then check if there is a way of incorporating logic of handling multiple source in service layer itself rather at api level. Commented Nov 13, 2018 at 4:47
  • I thought about that as well and I actually had the controller set up that way initially but I ran into the same issue since I am autowiring the implementations into the constructor. In this particular case, if I were to add a different mapping within the same controller for each source then I'd have even more boilerplate code. The only option with this approach would be to use a different controller for each source, which is doable ... but it seems more like a workaround than a solution but if I can't find anything better I may take that approach. Commented Nov 13, 2018 at 4:55
  • Check if you can design one service implementation which can handle the conditional logic for different sources thereby you can have logic in one class and one corresponding api by passing source param to serviceImpl. Commented Nov 13, 2018 at 5:05
  • Delegate the detection to the services instead. Inject a list of your services (interface). Then in your method loop through your list of services, and call a method to see if they can handle the request/source. When you found the correct one, use it. That way you have an extensible mechanism. Commented Nov 13, 2018 at 14:10

2 Answers 2

6

In Spring you can get all implementations of an interface (say T) by injecting a List<T> or a Map<String, T> field. In the second case the names of the beans will become the keys of the map. You could consider this if there are a lot of possible implementations or if they change often. Thanks to it you could add or remove an implementation without changing the controller.

Both injecting a List or a Map have some benefits and drawbacks in this case. If you inject a List you would probably need to add some method to map the name and the implementation. Something like :

interface MyInterface() {
    (...)
    String name()
}

This way you could transform it to a Map<String, MyInterface>, for example using Streams API. While this would be more explicit, it would polute your interface a bit (why should it be aware that there are multiple implementations?).

When using the Map you should probably name the beans explicitly or even introduce an annotation to follow the principle of least astonishment. If you are naming the beans by using the class name or the method name of the configuration class you could break the app by renaming those (and in effect changing the url), which is usually a safe operation to do.

A simplistic implementation in Spring Boot could look like this:

@SpringBootApplication
public class DynamicDependencyInjectionForMultipleImplementationsApplication {

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

    interface MyInterface {
        Object getStuff();
    }

    class Implementation1 implements MyInterface {
        @Override public Object getStuff() {
            return "foo";
        }
    }

    class Implementation2 implements MyInterface {
        @Override public Object getStuff() {
            return "bar";
        }
    }

    @Configuration
    class Config {

        @Bean("getFoo")
        Implementation1 implementation1() {
            return new Implementation1();
        }

        @Bean("getBar")
        Implementation2 implementation2() {
            return new Implementation2();
        }
    }



    @RestController
    class Controller {

        private final Map<String, MyInterface> implementations;

        Controller(Map<String, MyInterface> implementations) {
            this.implementations = implementations;
        }

        @GetMapping("/run/{beanName}")
        Object runSelectedImplementation(@PathVariable String beanName) {
            return Optional.ofNullable(implementations.get(beanName))
                           .orElseThrow(UnknownImplementation::new)
                           .getStuff();
        }

        @ResponseStatus(BAD_REQUEST)
        class UnknownImplementation extends RuntimeException {
        }

    }
}

It passes the following tests:

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class DynamicDependencyInjectionForMultipleImplementationsApplicationTests {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void shouldCallImplementation1() throws Exception {
        mockMvc.perform(get("/run/getFoo"))
                    .andExpect(status().isOk())
                    .andExpect(content().string(containsString("foo")));
    }

    @Test
    public void shouldCallImplementation2() throws Exception {
        mockMvc.perform(get("/run/getBar"))
                    .andExpect(status().isOk())
                    .andExpect(content().string(containsString("bar")));
    }

    @Test
    public void shouldRejectUnknownImplementations() throws Exception {
        mockMvc.perform(get("/run/getSomethingElse"))
               .andExpect(status().isBadRequest());
    }
}
Sign up to request clarification or add additional context in comments.

Comments

0

Regarding two of your doubts :
1. Instantiating the service object should not be an issue as this is one time job and controller gonna need them to serve all type of request.
2. You can use the exact Path mapping to get rid of switch case. For e.g. :

@GetMapping("/specificRequest/value1")
@GetMapping("/specificRequest/value2")
@GetMapping("/specificRequest/value3")

All of the above mapping will be on separate method which would deal with specific source value and invoke respective service method. Hope this will help to make code more cleaner and elegant.

There is one more option of separating this on service layer and having only one endpoint to serve all types of source but as you said there is different implementation for each source value then it says that source is nothing but a resource for your application and having separate URI/separate method makes the perfect sense here. Few advantages that I see here with this are :

  1. Makes it easy to write the test cases.
  2. Scaling the same without impacting any other source/service.
  3. Your code dealing the each source as separate entity from other sources.

The above approach should be fine when you have limited source values. If you have no control over source value then we need further redesign here by making source value differentiate by one more value like sourceType etc. and then having separate controller for each group type of source.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.