删除开关条件的干净代码(使用多态性)

Clean code for removing switch condition(using polymorphism)

正如 SOLID 原则所说,最好通过将它们转换为 类 和接口来删除开关条件。 我想用这段代码来做:

Note: This code is not real code and I just put my idea into it.

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)?!我应该再次将 switch 块放在这里还是做什么?

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

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

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

在这种情况下,您可以使用工厂来获取 Message 的实例。工厂将拥有 Message 的所有实例和 returns 基于 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(Open-Closed 原则)来保持你的代码干净。这意味着您的代码不应该在一个地方为每个 typeId 处理 实际处理逻辑

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

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

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

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

你可以这样做:

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

此服务是 "integration" 部分(相对于 "implementation":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 和 return 旧的;例如,这将允许您添加装饰器。

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

您可以使用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 的实例。

通常的简洁代码方法是让 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 然后 returns 是正确的类型,你的开关是

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

这样一来,您实际上就在 getMessageFromApi() 方法中进行了切换,因为它必须决定生成哪条消息。

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

可以一起使用Factory pattern and 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());
    }
});

如果需要,您可以创建一个 class MessageModelFactory 并使用私有 Visitor,而不是编写匿名 class。在那种情况下,将 Visitor 界面设置成这样可能会更好:

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

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

这样的实现 classes

要将MessageModels 转换为各种Messages,可以使用工厂,如 所示。除了选择 Message 到 return 的类型外,工厂还应使用 MessageModel.

填写每个类型的 Message 字段。