dimanche 7 août 2022

How to use the Java type system to create classes handling network messages, which determine their proper handler using a field in the message?

I have a lot of classes implementing a "common" interface called Setter.

public interface Setter {
  Result set(Config config, int entityId);

  enum Result {
    HANDLED, HANDLING_ERROR, REJECTED
  }
}

An example of an implementation looks like this, it sets in the world a 'number' value for a given 'entity' distinguished by its entityId:

public class NumberSetter implements Setter {

  private World world;
  private Assets assets;

  public NumberSetter(World world, Assets assets) {
    this.world = world.
    this.assets = assets;
  }

  @Override
  public Result set(Config config, int entityId) {
    if (config instanceof NumberConfig numberConfig) {
      world.passNumber(entityId, numberConfig.number);
      return Result.HANDLED;
    } else {
      return Result.REJECTED;
    }
  }
}

Please do notice, that the Config object is cast to a specific NumberConfig, otherwise the Setter implementation signals it didn't handle the argument.

I am using a Set of these Setters in a network-enabled class, where it tries to match a super-type Config object against one of these Setters from the Set. (The naming might be subject to change lmao.) The code below handles a network package by passing it to all of the Setters in the Set and checks if there were any errors or if no Setter handled the package. If the check passes then the package wasn't handled properly and the Handler returns a NOT_HANDLED which later crashes the program because I'm still at the development stage.

public class ConfigNetworkHandler implements NetworkHandler {

  private final Assets assets;
  private final Set<Setter> setterSet;

  public ConfigNetworkHandler(
      Assets assets,
      Set<Setter> setterSet
  ) {
    this.assets = assets;
    this.setterSet = setterSet;
  }

  @Override
  public boolean handle(WebSocket webSocket, int worldEntity, Component component) {
    var configId = ((ConfigId) component).getId();
    var config = assets.getConfigs().get(configId);
    var setterResults = setterSet.stream()
        .map(setter -> setter.set(config, worldEntity))
        .toList();
    var anyErrors = setterResults.stream().anyMatch(HANDLING_ERROR::equals);
    var wasHandled = setterResults.stream().anyMatch(HANDLED::equals);
    if (anyErrors || !wasHandled) {
      return NOT_HANDLED;
    }
    return FULLY_HANDLED;
  }
}

I don't like it how I am not using Java's type system properly. I don't know how to do it otherwise, without manually providing a Map between ConfigId's and the Setters, which I would rather not do, because the ConfigIds aren't known at compile-time. The NetworkHandler-type-stuff is kind of similar but there are a lot less of them and they will probably be refactored in a similar way (there is also a lot fewer of them, so it's not a practical issue).

I like the current solution because it allows me to add and remove Setters without worrying about the other ones and also I don't need to change the implementation of ConfigNetworkHandler, because it's provided a Set. I don't like it, because it requires list traversing, doesn't seem "idiomatic" for Java, returns weird Results instead of just not being called because it doesn't accept the type, and FEELS like there should be something else.

Do you have an idea how to approach this differently?

Aucun commentaire:

Enregistrer un commentaire