基于请求标识符的客户请求工厂

时间:2018-06-20 13:10:20

标签: java design-patterns dependency-injection refactoring

在我的程序中,我正在通过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),它可以根据收到的ID用于实例化ClientRequest实例吗?

1 个答案:

答案 0 :(得分:2)

将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或{ {1}}(例如Component)和这些bean所在的目录由组件扫描获取或在Service类中显式创建)。有关更多信息,请参见the Spring Framework Guru's article on Component Scanning


摘要

  • 在某种程度上,必须建立到@Configuration的ID映射
  • 在运行时建立映射会打开更多选项
  • Spring可用于解耦创建ClientRequest对象的工厂bean与ClientRequest之间的依赖关系