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:
@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();
}
}
@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;
}
}
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
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);
}
}
In the application, you can create an abstract class, say BasePizzaFactory with a factory method to create a pizza. You can then create a subclass of BasePizzaFactory , called PizzaFactory to implement the factory method. In the factory method, you can create and return a proper Pizza object.
To create a factory bean, all you have to do is to implement the FactoryBean interface by your creator bean class which will be creating actual other beans. Or to keep it simple, you can extend AbstractFactoryBean class.
Enabling @Autowired Annotations The Spring framework enables automatic dependency injection. In other words, by declaring all the bean dependencies in a Spring configuration file, Spring container can autowire relationships between collaborating beans. This is called Spring bean autowiring.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With