How to create a Simple Factory Pattern with autowired beans in Spring?
Asked Answered
C

1

9

I had a controller with 4 very similar methods, calling an API on a remote server to perform different actions on different types of users. What changed between these API calls are just the endpoint and some parameters.

Therefore, these 4 methods all called services with very similar code: they got a token from the server, set the parameters, return the API's response. Since more actions will be added later, I decided to use create a ServiceFactory using the Factory Method pattern and use the Template pattern on the services to avoid code duplication.

My problem is that in order for the factory to autowire the services, it needs to be coupled to them, I have to @Autowire every implementation. Is there a better solution?

Here's the code I have so far:

Rest Controller

@RestController
public class ActionController {
  @Autowired
  private SsoService ssoService;

  // this is the factory
  @Autowired
  private ServiceFactory factory;

  @PostMapping("/action")
  public MyResponse performAction(@RequestBody MyRequest request, HttpServletRequest req) {
    // template code (error treatment not included)
    request.setOperator(ssoService.getOperator(req));
    request.setDate(LocalDateTime.now());
    return serviceFactory.getService(request).do();
  }
}

Service Factory

@Component
public class ServiceFactory {

  @Autowired private ActivateUserService activateUserService;
  @Autowired private Action2UserType2Service anotherService;
  //etc

  public MyService getService(request) {
    if (Action.ACTIVATE.equals(request.getAction()) && UserType.USER.equals(request.getUserType()) {
      return activateUserService;
    }
    // etc
    return anotherService;
  }
}

Service Base, implementing the MyService interface

public abstract class ServiceBase implements MyService {

  @Autowired private ApiService apiService;
  @Autowired private ActionRepository actionRepository;
  @Value("${api.path}") private String path;

  @Override
  public MyResponse do(MyRequest request) {
    String url = path + getEndpoint();
    String token = apiService.getToken();

    Map<String, String> params = getParams(request);
    // adds the common params to the hashmap

    HttpResult result = apiService.post(url, params); 
    if (result.getStatusCode() == 200) {
      // saves the performed action
      actionRepository.save(getAction());
    }
    // extracts the response from the HttpResult
    return response;
  }
}

Service Implementation (there are 4)

@Service
public class ActivateUserService extends ServiceBase {
  @Value("${api.user.activate}")
  private String endpoint;

  @Override
  public String getEndpoint() {
    return endpoint;
  }

  @Override
  public Map<String,String> getParams(MyRequest request) {
    Map<String, String> params = new HashMap<>();
    // adds custom params
    return params;
  }

  @Override
  public Action getAction() {
    return new Action().type(ActionType.ACTIVATED).userType(UserType.USER);
  }
}
Conferva answered 10/1, 2019 at 17:50 Comment(2)
Seem that you are concerned about to inject many UserService implementation in the ServiceFactory,but from what you described and the codes you post , seems like that all 4 MyService implementations actually has the same logic , what are differences are just their configuration data. If yes , you can create a single implementation that accept a configuration parameter ....Esquimau
@KenChan ah, sorry it took so long to reply to your comment. Indeed, if only the configuration changed, it'd be better to use a configuration parameter. But it's not the only difference - despite being very similar, the implementation in the getParams method also changes. So having a single Service would also require a lot of ifs, or using a strategy pattern depending on which action is performed - which would be similar to the solution I've accepted.Conferva
S
13

You can @Autowired a List of MyService, which will create a List of all beans that implement the MyService interface. Then you can add a method to MyService that accepts a MyRequest object and decides if it can handle that request. You can then filter the List of MyService to find the first MyService object that can handle the request.

For example:

public interface MyService {

    public boolean canHandle(MyRequest request);

    // ...existing methods...
}

@Service
public class ActivateUserService extends ServiceBase {

    @Override
    public boolean canHandle(MyRequest request) {
        return Action.ACTIVATE.equals(request.getAction()) && UserType.USER.equals(request.getUserType());
    }

    // ...existing methods...
}

@Component
public class ServiceFactory {

    @Autowired
    private List<MyService> myServices;

    public Optional<MyService> getService(MyRequest request) {
        return myServices.stream()
            .filter(service -> service.canHandle(request))
            .findFirst();
    }
}

Note that the ServiceFactory implementation above uses Java 8+. If it is not possible to Java 8 or higher, you can implement the ServiceFactory class in the following manner:

@Component
public class ServiceFactory {

    @Autowired
    private List<MyService> myServices;

    public Optional<MyService> getService(MyRequest request) {

        for (MyService service: myServices) {
            if (service.canHandle(request)) {
                return Optional.of(service);
            }
        }

        return Optional.empty();
}

For more information on using @Autowired with List, see Autowire reference beans into list by type.


The core of this solution is moving the logic of deciding if a MyService implementation can handle a MyRequest from the ServiceFactory (an external client) to the MyService implementation itself.

Scheldt answered 10/1, 2019 at 18:9 Comment(3)
interesting, I really like this solution because it defers the decision of which service should handle the request to the services themselves, thanks a lot!Conferva
Clears the air. Was in great need. Thanks for sparing time for this and sharing for the community.Homoio
This is a nice solution with limited number of implementation and if you don't know horizontal scalability of your implementation but imo it has drawbacks 1. If there are too many implementation of MyService and probability of some implementation is very low 2. if canHandle method has some very deep logic to find out either implementation handle this request or not. So in this check every implementation is very heavy callKielce

© 2022 - 2024 — McMap. All rights reserved.