Issue
For example, there is a task, without violating the Open/Closed principle, to safely add new implementations for sending messages in different ways. The input comes with a parameter that contains the type of "transport" for sending messages or the device where the messages will arrive.
As an input parameter, you need to use Enum, but design the system so that you do not use switch when the parameter comes as a string, but immediately call the desired service in accordance with the stated conditions.
Here is my implementation.
- Test
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class NotificationMasterTest extends MultiImplementationInterfacesAndEnumApplicationTests {
@Autowired
private NotificationMaster notification;
@Test
void send() {
String phone = "phone";
TypeCommunication typeCommunication =
TypeCommunication.valueOf("phone");//Runtime Exception
notification.send(TypeCommunication.PHONE);
}
}
- enum
public enum TypeCommunication {
PHONE("phone"),
EMAIL("email"),
KAFKA("kafka");
private String value;
TypeCommunication(String value) {
this.value = value;
}
public String getType() {
return this.value;
}
}
- interface
public interface Notification {
void sendNotice();
TypeCommunication getType();
}
- PhoneImplementation
@Service
public class NotificationPhoneImpl implements Notification {
private final TypeCommunication typeCommunication = TypeCommunication.PHONE;
public NotificationPhoneImpl() {
}
@Override
public void sendNotice() {
System.out.println("Send through --- phone---");
}
@Override
public TypeCommunication getType() {
return this.typeCommunication;
}
}
- EmailEmplementation
@Service
public class NotificationEmailImpl implements Notification {
private final TypeCommunication typeCommunication = TypeCommunication.EMAIL;
@Override
public void sendNotice() {
System.out.println("Send through --- email ---");
}
@Override
public TypeCommunication getType() {
return this.typeCommunication;
}
}
- master
package com.example.multi.implementation.interfaces.services;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Component
public class NotificationMaster {
private Map<TypeCommunication, Notification> notificationService = new HashMap<>();
private Set<Notification> notification;
public NotificationMaster(Set<Notification> notification) {
this.notification = notification;
}
@PostConstruct
private void init(){
notification.stream()
.forEach(service -> {
TypeCommunication type = service.getType();
notificationService.put(type, service);
});
}
public void send(TypeCommunication typeCommunication) {
Notification notification = notificationService.get(typeCommunication);
notification.sendNotice();
}
}
I can't figure out how to pass a string and convert it to Enum on the fly (without using switch) and immediately get the desired implementation.
Maybe there is a more flexible solution in which Spring would have already prepared the components without me, so that I don't use Postconstruct and manually create a map with different service implementations?
Solution
I would say that it cannot be done "on the fly". So, I would create a static Map
and static method getByType
in TypeCommunication
to retrieve the enum by its value:
public enum TypeCommunication {
PHONE("phone"),
EMAIL("email"),
KAFKA("kafka");
private static final Map<String, TypeCommunication> TYPE_COMMUNICATION_MAP;
private final String value;
static {
TYPE_COMMUNICATION_MAP = Arrays.stream(TypeCommunication.values())
.collect(Collectors.toMap(TypeCommunication::getType, Function.identity(),
(existing, replacement) -> existing));
}
TypeCommunication(String value) {
this.value = value;
}
public String getType() {
return this.value;
}
public static TypeCommunication getByType(String type) {
return TYPE_COMMUNICATION_MAP.get(type);
}
}
If you received this value in the controller in the request body, then you could use @JsonValue
annotation on the getType()
method to immediately deserialize it in the enum by the value.
About your NotificationMaster
, spring cannot collect such a map automatically, but you can collect your map right in constructor without using @PostConstruct
, and using Collectors.toMap()
method, also take the EnumMap
, since your key is Enum
:
@Component
public class NotificationMaster {
private final Map<TypeCommunication, Notification> map;
public NotificationMaster(List<Notification> notifications) { // or Set
map = notifications.stream()
.collect(Collectors.toMap(Notification::getType, Function.identity(),
(existing, replacement) -> existing,
() -> new EnumMap<>(TypeCommunication.class)));
}
public void send(TypeCommunication typeCommunication) {
map.getOrDefault(typeCommunication, new DefaultNotificationImpl()).sendNotice();
}
private static class DefaultNotificationImpl implements Notification {
@Override
public void sendNotice() {
// some logic, e.g. throw new UnsupportedOperationException
}
@Override
public TypeCommunication getType() {
return null;
}
}
}
It also good to define the DefaultNotificationImpl
to avoid NullPointerException
in your send
method and using now Map#getOrDefault
method instead of Map#get
.
You can also take out the logic for creating a map in a separate configuration class and then simple autowire this map in NotificationMaster
:
@Configuration
public class Config {
@Bean
public Map<TypeCommunication, Notification> createMap(List<Notification> notifications) {
return notifications.stream()
.collect(Collectors.toMap(Notification::getType, Function.identity(),
(existing, replacement) -> existing,
() -> new EnumMap<>(TypeCommunication.class)));
}
}
@Component
public class NotificationMaster {
@Autowired
private Map<TypeCommunication, Notification> map;
...
}
Answered By - Georgy Lvov
Answer Checked By - Dawn Plyler (JavaFixing Volunteer)