首页 > 解决方案 > 基于请求标识符的客户端请求工厂

问题描述

在我的程序中,我通过 Java 套接字从客户端获取请求。每个请求都有一个唯一的命令标识符,对应于应用程序端的指定命令。

现在我有一个带有非常大开关的类,它根据接收到的命令 ID 创建命令类的实例。此类接收ByteBuffer来自客户端的请求数据和ClientConnection对象(表示客户端和服务器之间连接的类)。它从 中读取前两个字节ByteBuffer并获取相应的命令(扩展ClientRequest类的类的实例)。

例如:

public static ClientRequest handle(ByteBuffer data, ClientConnection client) {
    int id = data.getShort();  //here we getting command id
    switch (id) {
        case 1:
            return new CM_ACCOUNT_LOGIN(data, client, id);
        case 2:
            return new CM_ENTER_GAME(data, client, id);
        //...... a lot of other commands here
        case 1000:
            return new CM_EXIT_GAME(data, client, id);

    }
    //if command unknown - logging it
    logUnknownRequest(client, id);
    return null;
}

我不喜欢大开关结构。我的问题是:是否有一些方法可以重构此代码以使其更优雅?也许使用一些模式?

另外,将来我想尝试在我的程序中使用依赖注入(Guice),它可以用于ClientRequest根据收到的 ID 实例化实例吗?

标签: javadesign-patternsdependency-injectionrefactoring

解决方案


将 ID 映射到响应对象是一项常见任务,但很难摆脱以某种方式枚举哪个 ID 映射到特定响应对象。您提供的switch块有效,但它不是最可扩展的。例如,如果添加了新的响应对象或 ID,则必须将case语句添加到switch.

一种替代方法是创建 ID 映射到可以创建新响应对象的工厂对象。例如:

@FunctionalInterface
public interface ClientRequestFactory {
    public ClientRequest createClientRequest(ByteBuffer data, ClientConnection client, int id);
}

public class ClientRequestSwitchboard {

    private final Map<Integer, ClientRequestFactory> mappings = new HashMap<>();

    public ClientRequestSwitchboard() {
        mappings.put(1, (data, client, id) -> new CM_ACCOUNT_LOGIN(data, client, id));
        mappings.put(2, (data, client, id) -> new CM_ENTER_GAME(data, client, id));
        // ... Add each of the remaining request types ...
    }

    public ClientRequest createClientRequest(ByteBuffer data, ClientConnection client, int id) {
        ClientRequestFactory factory = mappings.get(id);

        if (factory == null) {
            return createDefault(data, client, id);
        }
        else {
            return factory.createClientRequest(data, client, id);
        }
    }

    protected ClientRequest createDefault(ByteBuffer data, ClientConnection client, int id) {
        logUnknownRequest(client, id);
        return null;
    }
}

然后,您可以使用ClientRequestSwitchboard如下:

private static final ClientRequestSwitchboard switchboard = new ClientRequestSwitchboard();

public static ClientRequest handle(ByteBuffer data, ClientConnection client) {
    int id = data.getShort();
    return switchboard.createClientRequest(data, client, id);
}

这种方法优于该switch技术的好处是您现在将映射信息存储为动态数据而不是静态case语句。在动态方法中,我们可以在运行时添加或删除映射,而不仅仅是在编译时(通过添加新case语句)。尽管这看起来可能略有不同,但动态方法使我们能够进一步改进解决方案。

如果我们使用依赖注入 (DI) 框架,例如 Spring,我们可以利用 Java 中的一些创造性功能。例如,我们可以ClientRequestFactory通过创建新ClientRequestFactory类来添加新实例(地图中的新条目)。例如:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ClientRequestFactoryForId {
    public int value();
}

@Service
@ClientRequestFactoryForId(1)
public class AccountLoginClientRequestFactory implements ClientRequestFactory {

    @Override
    public ClientRequest createClientRequest(ByteBuffer data, ClientConnection client, int id) {
        new CM_ACCOUNT_LOGIN(data, client, id);
    }
}

@Service
public class ClientRequestSwitchboard {

    private final Map<Integer, ClientRequestFactory> mappings = new HashMap<>();
    private final ListableBeanFactory beanFactory;

    @Autowired
    public ClientRequestSwitchboard(ListableBeanFactory beanFactory) {
        this.beanFactory = beanFactory;
    }

    @PostConstruct
    @SuppressWarnings("unchecked")
    private void findAllClientRequestFactories() {
        Map<String, Object> factories = beanFactory.getBeansWithAnnotation(ClientRequestFactoryForId.class);

        for (Object factory: factories.values()) {
            int id = dataStore.getClass().getAnnotation(ClientRequestFactoryForId.class).value();

            if (factory instanceof ClientRequestFactory) {
                mappings.put(id, (ClientRequestFactory) factory);
            }
            else {
                throw new IllegalStateException("Found object annotated as @ClientRequestFactoryForId but was not a ClientRequestFactory instance: " + factory.getClass().getName());
            }
        }
    }

    public ClientRequest createClientRequest(ByteBuffer data, ClientConnection client, int id) {
        ClientRequestFactory factory = mappings.get(id);

        if (factory == null) {
            return createDefault(data, client, id);
        }
        else {
            return request.createClientRequest(data, client, id);
        }
    }

    protected ClientRequest createDefault(ByteBuffer data, ClientConnection client, int id) {
        logUnknownRequest(client, id);
        return null;
    }
}

该技术使用 Spring 查找具有特定注释的所有类(在本例中ClientRequestFactoryForId为 ),并将每个类注册为可以创建ClientRequest对象的工厂。执行类型安全检查,因为我们不知道带有注释的对象是否ClientRequestFactoryForId实际实现ClientRequestFactory,即使我们期望它。要添加一个新工厂,我们只需创建一个带有ClientRequestFactoryForId注解的新 bean:

@Service
@ClientRequestFactoryForId(2)
public class AccountLoginClientRequestFactory implements ClientRequestFactory {

    @Override
    public ClientRequest createClientRequest(ByteBuffer data, ClientConnection client, int id) {
        new CM_ENTER_GAME(data, client, id);
    }
}

此解决方案假设ClientRequestSwitchboard和每个带有注释的类ClientRequestFactoryForId都是 Spring 应用程序上下文已知的 bean(使用Component或 的其他派生类进行注释Component,例如Service,并且这些 bean 存在的目录由组件扫描拾取或在类中显式创建@Configuration)。有关更多信息,请参阅Spring Framework Guru 关于组件扫描的文章


概括

  • 在某种程度上,ClientRequest必须建立映射的 ID
  • 在运行时建立映射打开了更多选项
  • Spring 可用于解耦创建ClientRequest对象的工厂 bean 和ClientRequestSwitchboard

推荐阅读