清除开关条件的代码(使用多态性)

Posted

技术标签:

【中文标题】清除开关条件的代码(使用多态性)【英文标题】:Clean code for removing switch condition(using polymorphism) 【发布时间】:2018-03-07 08:49:56 【问题描述】:

正如 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);

但是我怎么知道哪种类型与此消息相关以从中创建实例(PublicMessageStorableMessagePrivateMessage)?!我应该再次将开关块放在这里还是什么?

【问题讨论】:

提出一些想法。真的干净吗?或者只是被类和接口夸大了,你的动作是否被分成了 x 个类而不是一个简单的开关?这变得难以置信地难以遵循。因此,如果您开始考虑这到底有多干净,这可能会是一个令人惊讶的结局。 @Droidum 正如我在问题中提到的,这只是一个常规问题的模板,并不是真正的代码。在现实世界中,这可能更复杂,应该分成几个类。 即使只是一个模板,也可以考虑一下justSave/notify/notifyAll之间的关系。如果您也想保存 PublicMessagePrivateMessage,他们将需要访问 justSave 的功能,该功能要么移至单独的类 StorableMessage 并变得难以访问,要么它们仍然驻留在常见的Manager 类,这使得StorableMessage 的存在有点可疑。 notifyAll 也是如此,它可能与 notify 在循环中做同样的事情。这些类的存在只是为了存在 在务实的情况下,你犯了教条主义的错误。您想要实现的最终结果是可读的代码。您显示的代码很好,并且您在添加工厂或其他移动开关逻辑的方式方面没有取得太多成就。添加 2 个类而不获得可读性并不是胜利。当你有相同的分支逻辑重复时,这个 SOLID 原则就可以很好地发挥作用。这就是你认为“嗯......用多态性替换条件”重构的呼吁。那么引入一个包含 2 个以上方法的子类就完全有意义了。 不要为了能够使用原则而使用原则。了解何时需要它们以及何时不适用。任何重构都有成本/收益。 【参考方案1】:

你可以这样做:

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 并返回旧的;例如,这将允许您添加装饰器。

一个有用的例子是,如果您有一个不可靠的 Web 服务来提供处理;如果可以访问,则可以将其安装为处理程序;否则,将使用默认处理程序。

【讨论】:

这是一个可爱的移除开关的解决方案。但我认为这不是一个干净的代码。这个问题是我正在寻找最佳解决方案的常规问题 @SiamakFerdos:你为什么觉得它不干净? @EricDuminil 干净代码的重要部分之一是可读性。我认为我的问题是一个常规且简单的问题,可以有一个易于理解的代码。使用 Map 和 HashTable 更适合一些复杂的问题。所以我发现使用factory 更合适更通用。当然,可能是我做错了! @SiamakFerdos 我更新了答案以添加隐藏实际地图/消费者事物的服务。 只需将您的MessageHandlingService 重命名为MessageFactory。与整个主题的精神(无用的名称,以及与 f-word 相关的 OP 的顽固)的精神会更好【参考方案2】:

在这种情况下,您可以使用工厂来获取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(你不需要因为使用 switch 本身就不错)。 SOLID 试图说的是通过遵循 SRP(单一责任原则)和 OCP(开闭原则)来保持你的代码干净。这意味着您的代码不应该在一个地方为每个typeId 处理实际处理逻辑

使用工厂,您已将创建逻辑移至单独的位置,并且您已将实际处理逻辑移至相应的类。

编辑: 重申一下 - 我的回答侧重于 OP 的 SOLID 方面。通过拥有单独的处理程序类(来自 OP 的 Message 的一个实例),您可以实现 SRP。如果其中一个处理程序类发生更改,或者当您添加新的消息 typeId (message.typeId)(即添加新的 Message 实现)时,您无需修改​​原始的,因此您实现了 OCP。 (假设这些都不包含琐碎的代码)。这些已经在 OP 中完成了。

我在这里回答的真正意义是使用工厂来获得Message。这个想法是保持主应用程序代码干净,并将开关、if/else 和 new 运算符的使用限制在实例化代码中。 (类似于@Configuration 类/在 Guice 中使用 Spring 或 Abstract 模块时实例化 Bean 的类)。 OO 原则说使用开关是不好的。这取决于您使用它的哪里。在应用程序代码中使用它确实违反了 SOLID 原则,这就是我想要提出的。

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

【讨论】:

是的,我想是的,但我想确保这是解决此类常规问题的最佳解决方案 作者想摆脱 switch-case 语句,不想在我认为的其他地方封装它.. :x @Noixes 是的。但我的观点是没有必要摆脱它,这样做很好(这就是工厂的目的)。你可以做类似 daniu@ 所做的事情。但它只是以不同的方式隐藏它,您不会获得任何额外的(维​​护或可扩展性)好处。 一个 3 路 switch 变成了一个 3 路 switch 和 4 个类,其中每一个都有一个误导性的名称(好吧,它们源自 OP 的问题,但是仍然)。 Java 的“f-word”也不能离开。 @Voo,我在问题本身下方有另一条与此相关的评论。那些“3 个非常简单的类”很可能会以一种非常糟糕的方式交织在一起,或者依赖于一个提供所有功能的大类,并且已经存在。顺便说一句,代码分析工具不会维护/开发代码,人们会这样做。【参考方案3】:

这里的重点是将实例化和配置执行分开。

即使使用 OOP,我们也无法避免使用 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() 方法中进行切换,因为它必须决定生成哪条消息。

不过,这很好,因为它确实填充了消息类型 ID;并且客户端代码(您的交换机当前所在的位置)抵抗消息的更改;即添加另一个消息类型将被正确处理。

【讨论】:

【参考方案5】:

您遇到的真正问题是MessageModel 不是多态的。您需要将MessageModels 转换为多态Message 类,但您不应该在此类中放置任何处理消息的逻辑。相反,它应该包含消息的实际内容,并使用访问者模式,如Eric's Answer 所示,以便其他类可以对Message 进行操作。你不需要使用匿名Visitor;您可以创建像 MessageActionVisitor 这样的实现类。

要将MessageModels 转换为各种Messages,可以使用工厂,如user7's answer 所示。工厂除了选择返回哪种类型的Message外,还应使用MessageModel填写每种Message的字段。

【讨论】:

【参考方案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 ..
    

然后您可以调用该枚举的静态方法并创建您要管理的 Message 的实例。

【讨论】:

我不明白这有什么好处,尤其是当您使用clazz.newInstance() 时。为什么每次都创建一个新实例?如果必须在其中注入一些依赖项怎么办?...它只涉及在 工厂类(而不是在应用程序代码中)添加 switch case 与添加枚举。 如果你不想每次都创建一个新的实例,你可以在枚举初始化值的构造函数调用中创建实例。然后只返回实例而不是类。我认为这不是关于实现细节,而是关于概念和可能性。 :) 所有答案都很棒,并且针对同一问题显示了不同的方法。我也投了你的票:P ^^ 当然。那不是我关心的问题。依赖注入和枚举看起来不太好(我认为)。我想说的是 SOLID 原则并没有说要消除 switch/if,else。它只是努力保持应用程序代码干净。【参考方案7】:

您可以同时使用Factory pattern 和Visitor pattern。

你可以像这样创建一个工厂:

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

如果您愿意,您可以创建一个具有私有 Visitor 的类 MessageModelFactory,而不是编写匿名类,并使用它来代替。在这种情况下,最好将Visitor 接口设为这样:

interface Visitor<Result> 
    Result visit(StorableMessage message);
    Result visit(PublicMessage message);
    Result visit(PrivateMessage message);

【讨论】:

这没有回答“我如何知道与此消息相关的类型以从中创建实例?” 如果 MessageModel 接受访问者,访问者模式将起作用,但由于消息模型将其类型存储在整数中,而不是在其类类型中,因此需要在某处使用 switch 语句(或者如果是梯形图)。跨度> 哦,我明白了...所以问题是 MessageModel 来自第 3 方库,我们无法修改它以接受访问者...:/ 很公平。 我认为您的回答揭示了真正的问题:尝试直接在MessageModel 上操作。实际上,MessageModel 应该通过使用带有 switch 语句的工厂或构造函数转换为多态 Message。然后,而不是 Message 具有逻辑的类,应该在 Messages 上使用访问者模式,就像你展示的那样。 @Vaelus 啊,是的,我同意。听起来很明智。

以上是关于清除开关条件的代码(使用多态性)的主要内容,如果未能解决你的问题,请参考以下文章

3.多态

什么是多态(polymorphism)?

我不能在开关内沮丧吗? (斯威夫特 3)

多态指的是啥?

代码优化-多态代替IF条件判断

什么是多态