As SOLID principles say, it's better to remove switch conditions by converting them to classes and interfaces. I want to do it with this code:
Note: This code is not real code and I just put my idea into it.
MessageModel message = getMessageFromAnAPI();
manageMessage(message);
...
void manageMessage(MessageModel message){
switch(message.typeId) {
case 1: justSave(message); break;
case 2: notifyAll(message); break;
case 3: notify(message); break;
}
}
Now I want to remove switch statement. So I create some classes for it and I try to implement a polymorphism here:
interface Message{
void manageMessage(MessageModel message);
}
class StorableMessage implements Message{
@Override
public void manageMessage(MessageModel message) {
justSave(message);
}
}
class PublicMessage implements Message{
@Override
public void manageMessage(MessageModel message) {
notifyAll(message);
}
}
class PrivateMessage implements Message{
@Override
public void manageMessage(MessageModel message) {
notify(message);
}
}
and then I call my API to get my MessageModel
:
MessageModel message = getMessageFromAnAPI();
Now my problem is here. I have my model and I want manage it using my classes. As SOLID examples, I should do something like this:
PublicMessage message = new Message();
message.manageMessage(message);
But how can I know which type is related to this message to make an instance from it(PublicMessage
or StorableMessage
or PrivateMessage
)?! Should I put switch block here again to do it or what?
You can do this:
static final Map<Integer,Consumer<MessageModel>> handlers = new HashMap<>();
static {
handlers.put(1, m -> justSave(m));
handlers.put(2, m -> notifyAll(m));
handlers.put(3, m -> notify(m));
}
This will remove your switch to
Consumer<Message> consumer = handlers.get(message.typeId);
if (consumer != null) { consumer.accept(message); }
You should of course encapsulate this:
class MessageHandlingService implements Consumer<MessageModel> {
static final Map<Integer,Consumer<MessageModel>> handlers = new HashMap<>();
static {
handlers.put(1, m -> justSave(m));
handlers.put(2, m -> notifyAll(m));
handlers.put(3, m -> notify(m));
}
public void accept(MessageModel message) {
Consumer<Message> consumer = handlers.getOrDefault(message.typeId,
m -> throw new MessageNotSupportedException());
consumer.accept(message);
}
}
with your client code
message = getMessageFromApi();
messageHandlingService.accept(message);
This service is the "integration" part (as opposed to the "implementation": cfg Integration Operation Segregation Principle).
For a production environment with a CDI framework, this would look something like this:
interface MessageHandler extends Consumer<MessageModel> {}
@Component
class MessageHandlingService implements MessageHandler {
Map<Integer,MessageHandler> handlers = new ConcurrentHashMap<>();
@Autowired
private SavingService saveService;
@Autowired
private NotificationService notificationService;
@PostConstruct
public void init() {
handlers.put(1, saveService::save);
handlers.put(2, notificationService::notifyAll);
handlers.put(3, notificationService::notify);
}
public void accept(MessageModel m) { // as above }
}
One of the advantages of this vs the switch in @user7's answer is that the behavior can be adjusted at runtime. You can imagine methods like
public MessageHandler setMessageHandler(Integer id, MessageHandler newHandler);
which would install the given MessageHandler
and return the old one; this would allow you to add Decorators, for example.
An example for this being useful is if you have an unreliable web service supplying the handling; if it is accessible, it can be installed as a handlelr; otherwise, a default handler is used.