清除代码以删除开关条件(使用多态)

Sia*_*dos 14 java open-closed-principle solid-principles

正如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;
    }
}
Run Code Online (Sandbox Code Playgroud)

现在我想删除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);
    }
}
Run Code Online (Sandbox Code Playgroud)

然后我调用我的API来获取我的MessageModel:

MessageModel message = getMessageFromAnAPI();
Run Code Online (Sandbox Code Playgroud)

现在我的问题在这里.我有我的模型,我想用我的课来管理它.作为SOLID示例,我应该这样做:

PublicMessage message = new Message();
message.manageMessage(message);
Run Code Online (Sandbox Code Playgroud)

但是,我怎么知道哪个类型与此消息有关,以便从它(PublicMessageStorableMessagePrivateMessage)创建一个实例?!我应该把开关块放在这里再做一次还是什么?

dan*_*niu 18

你可以这样做:

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));
}
Run Code Online (Sandbox Code Playgroud)

这将删除您的开关

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

整合运作隔离原则

你当然应该封装这个:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

使用您的客户端代码

message = getMessageFromApi();
messageHandlingService.accept(message);
Run Code Online (Sandbox Code Playgroud)

该服务是"集成"部分(与"实现"相对: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 }
}
Run Code Online (Sandbox Code Playgroud)

可以在运行时更改行为

这与@ user7的答案中的切换相比的一个优点是可以在运行时调整行为.你可以想象像这样的方法

public MessageHandler setMessageHandler(Integer id, MessageHandler newHandler);
Run Code Online (Sandbox Code Playgroud)

哪个会安装给定的MessageHandler并返回旧的; 例如,这将允许您添加装饰器.

一个有用的例子是,如果你有一个不可靠的Web服务提供处理; 如果它是可访问的,它可以作为handlelr安装; 否则,使用默认处理程序.

  • 只需将```MessageHandlingService```重命名为```MessageFactory```.这整个主题的精神会更好(无用的名字,以及与f字相关的OP的hardon) (4认同)
  • @SiamakFerdos:为什么你觉得它不干净? (3认同)
  • 这是移除开关的一个可爱的解决方案。但我认为这不是一个干净的代码。这个问题是我经常遇到的问题,我正在寻找最佳解决方案 (2认同)

Tim*_*kle 10

这里的要点是您将实例化和配置执行分开.

即使使用OOP,我们也无法避免使用if/else级联或switch语句来区分不同的情况.毕竟我们必须创建专门的具体类的实例.
但这应该是在初始化代码或某种工厂.

业务逻辑中,我们希望通过在接口上调用泛型方法来避免if/else级联或switch语句,其中实现者更了解自己如何表现.

  • 是的,我认为这是最好的解决方案 (2认同)

use*_*er7 10

在这种情况下,您可以使用工厂来获取实例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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

调用代码看起来像

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

Message message = messageFactory.getMessage(messageModel);
message.manageMessage(messageModel);
Run Code Online (Sandbox Code Playgroud)

正如你所看到的,这并没有switch完全消除(你不需要因为使用开关本身并不坏).SOLID试图说的是通过遵循SRP(单一责任原则)和OCP(开放 - 封闭原则)来保持代码清洁.这意味着你的代码不应该在一个地方处理每个实际的处理逻辑typeId.

在工厂中,您已将创建逻辑移动到一个单独的位置,并且您已将实际处理逻辑移动到相应的类.

编辑: 重申一下 - 我的回答集中在OP的SOLID方面.通过具有单独的处理程序类(Message来自OP 的实例),您可以实现SRP.如果其中一个处理程序类发生更改,或者添加新消息typeId(message.typeId)(即添加新Message实现),则无需修改原始文件,从而实现OCP.(假设每个都不包含普通代码).这些已在OP中完成.

我在这里的答案的真正意义是使用工厂获得一个Message.我们的想法是保持主应用程序代码的清洁,并将开关,if/else和new运算符的用法限制为实例化代码.(类似于@Configuration类/在Guice中使用Spring或Abstract模块时实例化Beans的类).OO原则并未说使用交换机是坏事.这取决于在那里你使用它.在应用程序代码中使用它确实违反了SOLID原则,这就是我想要提出的.

我也喜欢daniu @的想法,使用功能方式,甚至可以在上面的工厂代码中使用(或者甚至可以使用简单的Map来摆脱开关).

  • 作者希望摆脱switch-case语句,并且不想将其封装在其他地方,我认为..:x (2认同)