Чистый код для удаления состояния переключателя (используя полиморфизм)

Как говорят принципы SOLID, лучше удалить условия переключения, преобразовав их в классы и интерфейсы. Я хочу сделать это с помощью этого кода:

Примечание. Этот код не является реальным, и я просто вложил в него свою идею.

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

Теперь я хочу удалить оператор switch. Поэтому я создаю для него несколько классов и пытаюсь реализовать полиморфизм здесь:

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

а потом я вызываю мой API, чтобы получить мой MessageModel:

MessageModel message = getMessageFromAnAPI();

Теперь моя проблема здесь. У меня есть моя модель, и я хочу управлять ею, используя мои занятия. В качестве твердых примеров я должен сделать что-то вроде этого:

PublicMessage message = new Message();
message.manageMessage(message);

Но как я могу узнать, какой тип связан с этим сообщением, чтобы сделать из него экземпляр (PublicMessage или же StorableMessage или же PrivateMessage)?! Должен ли я поставить блок переключателя здесь снова, чтобы сделать это или что?

7 ответов

Решение

В этом случае вы можете использовать фабрику, чтобы получить экземпляр Message, Фабрика будет иметь все экземпляры Message и возвращает соответствующий, основанный на typeId MessageModel.

class MessageFactory {
    private StorableMessage storableMessage;
    private PrivateMessage privateMessage;
    private PublicMessage publicMessage;
    //You can either create the above using new operator or inject it using some Dependency injection framework.

    public getMessage(MessageModel message) {
        switch(message.typeId) {
            case 1: return storableMessage; 
            case 2: return publicMessage;
            case 3: return privateMessage
            default: //Handle appropriately
        }
    }
}

Код вызова будет выглядеть

MessageFactory messageFactory; //Injected 
...
MessageModel messageModel = getMessageFromAnAPI();

Message message = messageFactory.getMessage(messageModel);
message.manageMessage(messageModel);

Как видите, от этого не избавились switch полностью (и вам не нужно, так как использование переключателя само по себе не плохо). SOLID пытается сказать, что вы должны поддерживать ваш код в чистоте, следуя здесь SRP (принципу единой ответственности) и OCP (принципу открытой и закрытой). Здесь это означает, что в вашем коде не должно быть действительной логики обработки для каждого typeId в одном месте.

С фабрикой вы переместили логику создания в отдельное место, и вы уже переместили фактическую логику обработки в соответствующие классы.

РЕДАКТИРОВАТЬ: Просто повторить - мой ответ сосредоточен на ТВЕРДОМ аспекте ОП. Наличие отдельных классов-обработчиков (экземпляр Message из ОП) вы добиваетесь ПСП. Если один из классов обработчика изменяется, или когда вы добавляете новое сообщение typeId (message.typeId) (т.е. добавить новый Message реализации) вам не нужно изменять оригинал и, следовательно, вы достигнете OCP. (При условии, что каждый из них не содержит тривиального кода). Это уже сделано в ОП.

Реальный смысл моего ответа здесь заключается в использовании фабрики, чтобы получить Message, Идея состоит в том, чтобы сохранить основной код приложения в чистоте и ограничить использование переключателей if / else и новых операторов кодом реализации. (Аналогично классам @Configuration / классам, которые создают экземпляры Beans при использовании модулей Spring или Abstract в Guice). Принципы ОО не говорят, что использование переключателей - это плохо. Это зависит от того, где вы его используете. Использование его в коде приложения нарушает принципы SOLID, и это то, что я хотел показать.

Мне также нравится идея от daniu @ использовать функциональный способ, и то же самое можно даже использовать в приведенном выше заводском коде (или даже использовать простую карту, чтобы избавиться от переключателя).

Вы можете сделать это:

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

Это удалит ваш переключатель в

Consumer<Message> consumer = handlers.get(message.typeId);
if (consumer != null) { consumer.accept(message); }

Принцип сегрегации операций интеграции

Вы должны, конечно, заключить в капсулу это:

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

с вашим клиентским кодом

message = getMessageFromApi();
messageHandlingService.accept(message);

Этот сервис является частью "интеграции" (в отличие от "реализации": принцип разделения операций интеграции cfg).

Со структурой CDI

Для производственной среды с платформой CDI это будет выглядеть примерно так:

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

Поведение может быть изменено во время выполнения

Одним из преимуществ этого по сравнению с переключателем в ответе @user7 является то, что поведение можно настроить во время выполнения. Вы можете представить себе такие методы, как

public MessageHandler setMessageHandler(Integer id, MessageHandler newHandler);

который бы установил данный MessageHandler и вернуть старый; это позволит вам добавить декораторы, например.

Примером того, что это полезно, является наличие ненадежного веб-сервиса, предоставляющего обработку; если он доступен, его можно установить как handlelr; в противном случае используется обработчик по умолчанию.

Суть в том, что вы отделяете инстанцирование и конфигурацию от исполнения.

Даже с ООП мы не можем избежать различий между различными случаями, используя if/else каскады или switch заявления. Ведь нам нужно создавать экземпляры специализированных конкретных классов.
Но это должно быть в коде инициализации или какой-то фабрике.

В рамках бизнес-логики мы хотим избежать if/else каскады или switch заявления, вызывая универсальные методы на интерфейсах, где разработчик лучше знает, как себя вести.

Обычный подход чистого кода для MessageModel должен содержать его поведение.

interface Message {
    void manage();
}

abstract class MessageModel implements Message {
}

public class StoringMessage extends MessageModel {
    public void manage() {
        store();
    }
}
public class NotifyingMessage extends MessageModel {
    public void manage() {
        notify();
    }
}

Ваш getMessageFromApi затем возвращает правильный тип, и ваш переключатель

MessageModel model = getMessageFromApi();
model.manage();

Таким образом, вы по существу имеете переключатель в getMessageFromApi() метод, потому что он должен решить, какое сообщение генерировать.

Тем не менее, это хорошо, потому что он все равно заполняет идентификатор типа сообщения; и код клиента (где в данный момент находится ваш коммутатор) устойчив к изменениям в сообщениях; т.е. добавление другого типа сообщения будет обработано правильно.

Реальная проблема у вас в том, что MessageModel не полиморфный Вам нужно конвертировать MessageModelс полиморфным Message класс, но вы не должны помещать логику того, что делать с сообщениями в этом классе. Вместо этого он должен содержать фактическое содержимое сообщения и использовать шаблон посетителя, как показано в ответе Эрика, чтобы другие классы могли работать с Message, Вам не нужно использовать анонимный Visitor; Вы можете создавать реализующие классы, такие как MessageActionVisitor,

Преобразовать MessageModelс различными Messages, вы можете использовать фабрику, как показано в ответе пользователя user7. В дополнение к выбору, какой тип Message чтобы вернуться, фабрика должна заполнить поля каждого типа Message с использованием MessageModel,

Вы можете использовать фабричный шаблон. Я бы добавил перечисление со значениями:

public enum MessageFacotry{
    STORING(StoringMessage.TYPE, StoringMessage.class),
    PUBLIC_MESSAGE(PublicMessage.TYPE, PublicMessage.class),
    PRIVATE_MESSAGE(PrivateMessage.TYPE, PrivateMessage.class);
    Class<? extends Message> clazz;
    int type;
    private MessageFactory(int type, Class<? extends Message> clazz){
        this.clazz = clazz;
        this.type = type;
    }

    public static Message getMessageByType(int type){

         for(MessageFactory mf : values()){
              if(mf.type == type){
                   return mf.clazz.newInstance();
              }
         }
         throw new ..
    }
}

Затем вы можете вызвать статический метод этого перечисления и создать экземпляр сообщения, которым вы хотите управлять.

Вы можете использовать шаблон Factory и шаблон Visitor вместе.

Вы можете создать фабрику следующим образом:

class MessageFactory {
    public Message getMessage(MessageModel message) {
        switch(message.typeId) {
            case 1: return new StorableMessage((MessageModelType1) message);
            case 2: return new PrivateMessage((MessageModelType2) message);
            case 3: return new PublicMessage((MessageModelType3) message);
            default: throw new IllegalArgumentException("unhandled message type");
        }
    }
}

и объявите ваши сообщения так:

interface Message {
    void accept(Visitor visitor);
}

class StorableMessage implements Message {

    private final MessageType1 message;

    public StorableMessage(MessageModelType1 message) {
        this.message = message;
    }

    @Override
    public <Result> Result accept(Visitor<Result> visitor) {
        return visitor.visit(this);
    }

    public MessageModelType1 getMessage() {
        return message;
    }
}

class PublicMessage implements Message {
    ...
}

class PrivateMessage implements Message {
    ...
}

и объявить Visitor как это:

interface Visitor {
    void visit(StorableMessage message);
    void visit(PublicMessage message);
    void visit(PrivateMessage message);
}

и замените ваши операторы switch следующим:

Message message = ....;

message.accept(new Visitor() {
    @Override
    public void visit(StorableMessage message) {
        justSave(message.getMessage());
    }

    @Override
    public void visit(PublicMessage message) {
        notifyAll(message.getMessage());
    }

    @Override
    public void visit(PrivateMessage message) {
        notify(message.getMessage());
    }
});

Если вы хотите, вместо того чтобы писать анонимный класс, вы можете создать класс MessageModelFactory это имеет частный Visitorи используйте это вместо этого. в этом случае может быть лучше сделать Visitor интерфейс, как это:

interface Visitor<Result> {
    Result visit(StorableMessage message);
    Result visit(PublicMessage message);
    Result visit(PrivateMessage message);
}
Другие вопросы по тегам