Java: How to do a Factory for Singletons within Spring

in #java5 years ago

spring.png

For this post I wanted to go over a bit of a conundrum that I faced not too long ago. I began writing an auto trading program utilizing the Robinhood trading platform. For this program, there would need to be a lot of REST client calls, so I wanted to build in a good design to be able to add more client calls easily and quickly. The solution I came up with was to have an interface that would define a client service call (which are also singletons), and to return anything that implemented this interface from behind a factory. In a typical factory the returned object would be created brand new. But, I wanted Spring to manage both the factory and the service call beans for me. This would pose a problem, because Spring would need to create all of the beans on application startup, and if my factory created new objects then Spring wouldn't know what to do with them and wouldn't be able to autowire into where they needed to go.

Now that the problem has been stated, let's jump into some code on how to overcome these obstacles. The first thing to do is to create the interface that each client call will need to implement.

public interface ClientService {
    ServiceCall getServiceCall();
    RestResponse send(Payload payload);
}

So, let's look at this interface a bit closer so we can understand all of the pieces. The first method that is declared returns a ServiceCall. For us, this will be a basic enum whose purpose is to allow the implementors of this interface identify themselves. We'll see how that works when we make some sample classes that do that. The second method is used to send a rest request to the Robinhood API and return the response. We will define both RestResponse and Payload to be abstract classes that we can extend to contain the data that each request and response will expect.

Now that we have gone over the pieces, let's code them out! The first one up is the ServiceCall enum. Our example will have 3 generic services that we'll define soon.

public enum ServiceCall {
    ACCOUNT_SERVICE, LOGIN_SERVICE, POSITIONS_SERVICE
}

Next up are the abstract classes that will service as the top level request and response objects for our send method.

public abstract class RestResponse {
}
public abstract class Payload {
}

Now that we have our interface and supporting classes defined, we can go ahead and create our 3 services that implement our interface. For the sake of brevity, assume that for each service there is a corresponding request and response class that extend the appropriate abstract classes. Also assume that each service may have their own dependencies injected in to be used, such as object mappers, http client libraries, etc.

@Service
public class AccountService implements ClientService {
    public ServiceCall getServiceCall() {
        return ServiceCall.ACCOUNT_SERVICE;
    }

    public RestResponse send(Payload payload) {
        RestResponse response = null;
        // Do stuff to send and receive via REST call for this service (and populate the response object!!)
        return response;
    }
}
@Service
public class LoginService implements ClientService {
    public ServiceCall getServiceCall() {
        return ServiceCall.LOGIN_SERVICE;
    }

    public RestResponse send(Payload payload) {
        RestResponse response = null;
        // Do stuff to send and receive via REST call for this service (and populate the response object!!)
        return response;
    }
}
@Service
public class PositionsService implements ClientService {
    public ServiceCall getServiceCall() {
        return ServiceCall.POSITIONS_SERVICE;
    }

    public RestResponse send(Payload payload) {
        RestResponse response = null;
        // Do stuff to send and receive via REST call for this service (and populate the response object!!)
        return response;
    }
}

So, now that we have that, let's talk through a couple of key points here. First, we need Spring to manage these singletons for us, so we have to have the @Service annotation (or @Component if you prefer). Second, each service needs to use a different option in the ServiceCall enum. If 2 services return the same ServiceCall, then our factory will have unexpected results, which we will walk through next.

Now that we have some services, let's write a factory class that will return the service when needed. There's going to be some magic at work here, but I'll explain what is going on after we get the code completed.

@Service
public class ServiceFactory {

    private List<ClientService> clientServices;

    private static final Map<ServiceOptions, ClientService> cachedServices = new HashMap<>();

    @Autowired
    public ServiceFactory(List<ClientService> clientServices) {
        this.clientServices = clientServices;
    }

    @PostConstruct
    private void populateCache() {
        for (ClientService service : clientServices) {
            cachedServices.put(service.getServiceCall(), service);
        }
    }

    public ClientService getService(ServiceCall serviceCall) {
        if (cachedServices.containsKey(serviceCall)) {
            return cachedServices.get(serviceCall);
        }
        
        throw new InvalidServiceException("Unknown service call: " + serviceCall);
    }
}

And there you have it! Now, to explain what all is going on with this factory. First, the factory itself is managed by Spring due to the @Service annotation. Next, any Spring managed bean that implements the ClientService interface is auto-wired into the clientServices list. Our factory needs to be able to return the correct service that the caller wants, so we need to identify each service, which is where the Map comes into play. The @PostConstruct annotation will kick off the populateCache() method after the factory is created, which will populate our Map with the ServiceCall as the key, and the actual service as the value. This is why it is important to not have 2 classes that implement our ClientService interface use the same ServiceCall option. After the Map is populated, all that is left is the getService method. Here, the only thing to note is that I check to make sure my map contains the ServiceCall being requested for, and if not I create a custom RuntimeException called InvalidServiceException.

Before wrapping up this post, let's take a quick look at how we might use this factory.

public class MyDelegate {
    
    private ServiceFactory serviceFactory;

    @Autowired
    public MyDelegate(ServiceFactory serviceFactory) {
        this.serviceFactory = serviceFactory;
    }

    public void someMethod() {
        // Do stuff before needing to send a rest request

        serviceFactory.getService(ServiceCall.ACCOUNT_SERVICE).send(accountPayload);

        // Do stuff after receiving a response
    }
}

Well, that's pretty much it. Just to recap, we've created a generic interface, request and response classes, and made some classes that implement out interface. Then, we introduced a factory as a way of retrieving the service that we want. Finally, we have an example on how we can utilize the factory in a clean way. A few closing remarks to keep in mind. Each class that implements our interface has to be annotated with @Service or @Component to be managed by Spring. Spring will autowire anything that it is managing that implements the interface into a List. The @PostConstuct annotation will kick off after an object is created, which we use to populate our Map, which in turn gives each of our services a key that can be used to identify the service.

If we want to add more service calls, all that we need to do is add a new option to the ServiceCall enum and create a new service class that implements our ClientService interface. We don't have to touch the factory at all, since Spring will use its magic to autowire for us!

Please let me know what you thing of this design approach by leaving a comment below.

Sort:  

Congratulations @altlash! You received a personal award!

Happy Birthday! - You are on the Steem blockchain for 2 years!

You can view your badges on your Steem Board and compare to others on the Steem Ranking

Vote for @Steemitboard as a witness to get one more award and increased upvotes!