Чистый код для удаления состояния переключателя (используя полиморфизм)
Как говорят принципы 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
с различными Message
s, вы можете использовать фабрику, как показано в ответе пользователя 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);
}