0
votes

Apologies if my terminology is jumbled up, I'm rather new to Spring.

I have inherited an application that has a rather unappealing approach as follows:

  • The app deals with 3 types of data (T1, T2, T3). The types are in Enum class DataType.

  • There is a data service class hierarchy, with abstract DataService class, and 3 derived classes (one per DataType) T1DataService, T2DataService ....

  • The config creates 3 beans for the Data Service:

    @Bean(name = "T1DataService")
    public DataService getDataService() throws Exception {
        return new T1DataService();
    }
    
    @Bean(name = "T2DataService")
    public DataService getDataService() throws Exception {
        return new T2DataService();
    }
    
    @Bean(name = "T3DataService")
    public DataService getDataService() throws Exception {
        return new T3DataService();
    }
    
  • Similarly, there is a compute service, with only 1 class, but 3 bean instances, where the data type is passed to each instance's constructor:

    @Bean(name = "T1ComputeService")
    public ComputeService getComputeService() throws Exception {
        return new ComputeService (T1);
    }
    
    @Bean(name = "T2ComputeService")
    public ComputeService getComputeService() throws Exception {
        return new ComputeService (T2);
    }
    
    @Bean(name = "T3ComputeService")
    public ComputeService getComputeService() throws Exception {
        return new ComputeService (T3);
    }
    
  • There is a request processor class, which needs data from the DataService, as well as computations from ComputeService, based on the data type specified in the request.

    The way it was coded, was to Autowire all 3 data and 3 compute service beans:

    @Autowired @Qualifier("T1DataService")
    protected DataService T1DataService;
    
    @Autowired @Qualifier("T2DataService")
    protected DataService T2DataService;
    
    @Autowired @Qualifier("T3DataService")
    protected DataService T3DataService;
    
    @Autowired @Qualifier("T1ComputeService")
    protected ComputeService T1ComputeService;
    
    @Autowired @Qualifier("T2ComputeService")
    protected ComputeService T2ComputeService;
    
    @Autowired @Qualifier("T3ComputeService")
    protected ComputeService T3ComputeService;
    

    And then, in the code of the processor class:

    DataService dataService = null;
    ComputeService computeService = null;
    
    switch (request.dataType) {
    case T1: 
        dataService = T1DataService;
        computeService = T1ComputeService;
        break;
    case T2: 
        dataService = T2DataService;
        computeService = T2ComputeService;
        break;
    case T3: 
        dataService = T3DataService;
        computeService = T3ComputeService;
        break;
    

This seems like a REALLY REALLY bad way of picking the correct object.

If these weren't autowired Spring beans, but regular POJOs, I would have simply stashed them in a HashMap

static HashMap<DataType, DataService> dataServices = new HashMap<>();
dataServices.put(T1, new T1DataService());
dataServices.put(T2, new T2DataService());
dataServices.put(T3, new T3DataService());

static HashMap<DataType, ComputeService> computeServices = new HashMap<>();
for (DataType dataType : DataType.allDataTypes()) {
    computeServices.put(dataType , new ComputeService(dataType));
}

... and then in the processor class, access the correct service object by getting it from HashMap by a key:

DataService dataService = dataServices.get(request.dataType);
ComputeService computeService = computeServices.get(request.dataType);

Is there any way I can achieve a code similarly clean and concise using Spring?

2
I'm pretty new to Spring, so I apologize if I'm not using correct terminology for what I need.DVK
@RC - how would that work for compute services? getBean gets the bean for correct class; but compute services are all 3 beans of same class.DVK

2 Answers

0
votes

I would put the bean name or class in the enum and use ApplicationContext#getBean (with lombok annotations):

@RequiredArgsConstructor
enum DataType {
   T1("T1DataService", "T1ComputeService"),
   T2("T2DataService", "T2ComputeService"),
   T3("T3DataService", "T3ComputeService");

   @Getter
   private final String dataServiceName;

   @Getter
   private final String computeServiceName;   
}

then

DataService dataService = applicationContext.getBean(DataType.T1.getDataServiceName());
ComputeService computeService = applicationContext.getBean(DataType.T1.getComputeServiceName());

(something like that, note that you could also assume the bean name is normalized and do a DataType.T1.name() + "DataService")

0
votes

There are a few things that needs to happen here:

  • You need to inject the various services somewhere.
  • You need to recover the proper service based on the enum value

You could do fetch stuff from applicationContext, but imo that would a step backwards and away from annotation-based configuration.

When it comes to ComputeService, is it really necessary to introduce the enum constant as state? If this could be generalized, and instead check which enum constant parameters to the class' methods holds, you could get down to only one instance of this class.

Then, you would have something like this:

public class Something {

    @Inject
    private ComputeService computeService;

    @Inject
    @Named("T1DataService")
    private DataService T1DataService;

    @Inject
    @Named("T2DataService")
    private DataService T2DataService;

    @Inject
    @Named("T3DataService")
    private DataService T3DataService;

    //...
}

Further, there is nothing keeping you from populating a map with the injected services, for easier lookup:

private Map<DataType, DataService> serviceMap;

@PostConstruct
public void createMap() {
    serviceMap = new HashMap<>();
    serviceMap.put(T1, T1DataService);
    serviceMap.put(T2, T2DataService);
    serviceMap.put(T3, T3DataService);
}

public void doStuff(final Request request) {
    serviceMap.get(request.datatype).doStuff();
}