Очистить код для удаления условия переключения (с использованием полиморфизма)
Как утверждают принципы 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();
Теперь моя проблема здесь. У меня есть моя модель, и я хочу управлять ею, используя свои классы. Как примеры SOLID, я должен сделать что-то вроде этого:
PublicMessage message = new Message();
message.manageMessage(message);
Но как я могу узнать, какой тип связан с этим сообщением, чтобы сделать экземпляр из него (PublicMessage
или StorableMessage
или PrivateMessage
)?! Должен ли я снова включить блок блокировки, чтобы сделать это или что?
Ответы
Ответ 1
Вы можете использовать фабрику в этом случае, чтобы получить экземпляр Message
. Завод будет иметь все экземпляры Message
и возвращает соответствующий на основе типа MessageModel typeId.
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
в одном месте.
С фабрикой вы переместили логику создания в отдельное место, и вы уже перевели фактическую логику обработки в соответствующие классы.
РЕДАКТИРОВАТЬ: просто повторюсь - мой ответ фокусируется на SOLID аспекте OP. Имея отдельные классы обработчиков (экземпляр Message
из OP), вы достигаете SRP. Если изменяется один из классов обработчика или когда вы добавляете новое сообщение typeId (message.typeId
) (то есть добавляете новую реализацию Message
), вам не нужно изменять оригинал и, следовательно, вы достигаете OCP. (При условии, что каждый из них не содержит тривиального кода). Это уже сделано в ОП.
Реальная точка моего ответа здесь заключается в том, чтобы использовать Factory для получения Message
. Идея состоит в том, чтобы сохранить основной код приложения чистым и ограничить использование коммутаторов if/else и новых операторов кодом создания. (Подобно классам @Configuration/классам, которые создают экземпляр Beans при использовании Spring или абстрактных модулей в Guice). Принципы OO не говорят, что использование переключателей является плохим. Это зависит от того, где вы его используете. Использование его в коде приложения нарушает принципы SOLID, и именно это я хотел бы выявить.
Мне также нравится идея от daniu @использовать функциональный способ, и то же самое можно даже использовать в вышеупомянутом заводском коде (или даже использовать простую карту, чтобы избавиться от коммутатора).
Ответ 2
Вы можете сделать это:
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 Integration Operation Segregation Principle).
С системой 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
и вернет старый; это позволит вам добавить Decorators, например.
Примером этого является полезность, если у вас есть ненадежный веб-сервис, обеспечивающий обработку; если он доступен, он может быть установлен как handlelr; в противном случае используется обработчик по умолчанию.
Ответ 3
Главное здесь, что вы отделяете экземпляр и конфигурацию от исполнения.
Даже при ООП мы не можем избежать различий между различными случаями, используя каскады if/else
или инструкции switch
. В конце концов, мы должны создавать экземпляры специализированных конкретных классов.
Но это должно быть в коде инициализации или какой-то фабрике.
В рамках бизнес-логики мы хотим избежать if/else
каскадов или операторов switch
, вызывая общие методы на интерфейсах, где разработчик лучше знает себя, как себя вести.
Ответ 4
Обычный метод чистого кода предназначен для того, чтобы 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()
потому что он должен решить, какое сообщение нужно сгенерировать.
Однако это нормально, потому что он все равно заполняет идентификатор типа сообщения; и клиентский код (где в настоящий момент находится ваш коммутатор) сопротивляется изменениям в сообщениях; т.е. добавление другого типа сообщения будет обработано правильно.
Ответ 5
Реальная проблема заключается в том, что MessageModel
не является полиморфным. Вам нужно преобразовать MessageModel
в полиморфный класс Message
, но вы не должны ставить какую-либо логику того, что делать с сообщениями этого класса. Вместо этого он должен содержать фактическое содержимое сообщения и использовать шаблон посетителя, как показано в Eric Answer, чтобы другие классы могли работать с Message
. Вам не нужно использовать анонимного Visitor
; вы можете создавать классы реализации, такие как MessageActionVisitor
.
Чтобы преобразовать MessageModel
в различные Message
s, вы можете использовать фабрику, как показано в ответе user7. Помимо выбора типа Message
для возврата, фабрика должна заполнить поля каждого типа Message
с помощью MessageModel
.
Ответ 6
Вы можете использовать Factory Pattern. Я бы добавил перечисление, которое имеет значения:
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 ..
}
}
Затем вы можете вызвать статический метод этого перечисления и создать экземпляр сообщения, которое хотите управлять.
Ответ 7
Вы можете использовать шаблон 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);
}
и замените операторы коммутатора следующим:
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);
}