Clean code for removing switch condition(using polymorphism)

Siamak Ferdos picture Siamak Ferdos · Mar 7, 2018 · Viewed 7.8k times · Source

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?

Answer

daniu picture daniu · Mar 7, 2018

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); }

Integration Operation Segregation Principle

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).

With a CDI framework

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 }
}

Behavior can be changed at Runtime

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.