Let’s take an example & understand the problem first.
We have one VehicleService interface:
public interface VehicleService {
public void drive();
}
We have two different implementations of VehicleService:
@Component
public class TwoWheelerService implements VehicleService {
public void drive() {
// drive two wheeler
}
}
@Component
public class FourWheelerService implements VehicleService {
public void drive() {
// drive four wheeler
}
}
We have a RestController:
@RestController
public class VehicleController {
@RequestMapping(path = "/drive")
public void drive(@RequestParam(name = "type") TYPE type) {
// call appropriate VehicleService implementation based on the "type" query
// parameter
}
}
So you got the problem. I need to use either TwoWheelerService or FourWheelerService based on the “type” query parameter in the Controller method.
We can do that easily. We can just autowire both implementations in RestController. Then add an if condition in the Controller method.
@RestController
public class VehicleController {
@Autowired
private TwoWheelerService twoWheelerService;
@Autowired
private FourWheelerService fourWheelerService;
@RequestMapping(path = "/drive")
public void drive(@RequestParam(name = "type") TYPE type) {
if (type == TYPE.TWO_WHEELER) {
twoWheelerService.drive();
} else {
fourWheelerService.drive();
}
}
}
Any code similar to above would just work fine. But what happens when you add a new service implementation, let’s say for THREE_WHEELER? You would have to autowire the service & modify Controller method. Basically you would have to make changes to multiple places in your existing code.
That is error-prone. Code should be open for extension, but close for modification. We have heard it before.
So how can we do it in Spring framework? You might have faced this problem while writing your Spring Boot application. There are multiple ways to do it. Here I will show one way where we will use Spring based annotation & take some help from a design pattern.
Our idea is to implement a modified version of Registry Pattern. So what is a registry? In simple terms, registry is a key value pair. You can add or delete items in the registry. And you can also find the value given a key.
We will be building a service registry where a concrete service class instance can be found by its type:
@Component
public class VehicleServiceRegistry {
private final Map<TYPE, VehicleService> serviceRegistry = new HashMap<TYPE, VehicleService>();
@Autowired
public VehicleServiceRegistry(List<VehicleService> vehicleServices)
{
vehicleServices.forEach(vehicleService -> serviceRegistry.put(vehicleService.getType(), vehicleService));
}
public VehicleService getVehicleService(TYPE type) {
return serviceRegistry.get(type);
}
}
If you see above service registry code, you should be able to figure out that we are adding vehicle type as key & concrete implementation as value in an internal map. We have a method to get vehicle service instance by type. We don’t need a delete operation here, so no method was provided. And we are using Spring @Autowired annotation to inject all concrete implementations of VehicleService as a list.
There is a new service method we have used in registry vehicleService.getType(). So our service interface should look like below:
public interface VehicleService {
public void drive();
public TYPE getType();
}
Concrete service implementations should be like below:
@Component
public class TwoWheelerService implements VehicleService {
public void drive() {
// drive two wheeler
}
public TYPE getType() {
return TYPE.TWO_WHEELER;
}
}
@Component
public class FourWheelerService implements VehicleService {
public void drive() {
// drive four wheeler
}
public TYPE getType() {
return TYPE.FOUR_WHEELER;
}
}
Now the question is what is the advantage of creating this service registry. Just look at the new RestController code below:
@RestController
public class VehicleController {
@Autowired
private VehicleServiceRegistry vehicleServiceRegistry;
@RequestMapping(path = "/drive")
public void drive(@RequestParam(name = "type") TYPE type) {
vehicleServiceRegistry.getVehicleService(type).drive();
}
}
Controller code is clean now. Controller doesn’t know about any concrete service implementation. We have programmed to an interface, not to an implementation. Advantage is pretty clear. When you add a new Spring service for three wheeler, you will just have to write the new service class & add corresponding enum value. You don’t have to modify any existing code in any other place. Scope of change is minimal, so your testing is also minimal. You can be pretty confident that your existing functionalities will work fine as you didn’t modify any existing code.
This way you can dynamically inject a concrete service implementation runtime in Spring framework. And your code will remain clean & easy to maintain.