Diseño de código: delegación de funciones arbitrarias

9

En PPCG, con frecuencia tenemos desafíos de King of the Hill , que enfrentan diferentes robots de código entre sí. No nos gusta limitar estos desafíos a un solo idioma, por lo que hacemos comunicación multiplataforma sobre E / S estándar.

Mi objetivo es escribir un marco que los escritores de desafíos puedan usar para facilitar la escritura de estos desafíos. He elaborado los siguientes requisitos que me gustaría cumplir:

  1. El escritor de desafíos puede hacer una clase donde los métodos representan cada una de las comunicaciones distintas . Por ejemplo, en nuestro desafío Good vs Evil , el escritor haría una Playerclase que tenga un abstract boolean vote(List<List<Boolean>> history)método.

  2. El controlador puede proporcionar instancias de la clase anterior que se comunican a través de E / S estándar cuando se invocan los métodos mencionados anteriormente . Dicho esto, no todas las instancias de la clase anterior se comunicarán necesariamente a través de E / S estándar. 3 de los bots pueden ser bots nativos de Java (que simplemente anulan la Playerclase, donde otros 2 están en otro idioma)

  3. Los métodos no siempre tendrán el mismo número de argumentos (ni siempre tendrán un valor de retorno)

  4. Me gustaría que el escritor de desafíos tenga que hacer el menor trabajo posible para trabajar con mi marco.

No estoy en contra de usar la reflexión para resolver estos problemas. He considerado pedirle al escritor del desafío que haga algo como:

class PlayerComm extends Player {
    private Communicator communicator;
    public PlayerComm(Communicator communicator){
        this.communicator = communicator;
    }
    @Override
    boolean vote(List<List<Boolean>> history){
         return (Boolean)communicator.sendMessage(history);
    }
}

pero si hay varios métodos, esto puede ser bastante repetitivo, y el casting constante no es divertido. ( sendMessageen este ejemplo aceptaría un número variable de Objectargumentos y devolvería un Object)

¿Hay una mejor manera de hacer esto?

Nathan Merrill
fuente
1
Estoy confundido acerca de la parte " PlayerComm extends Player". ¿Se están extendiendo todos los entrantes de Java Player, y esta PlayerCommclase es un adaptador para entrantes que no son de Java?
ZeroOne
Sí, eso es correcto
Nathan Merrill
Entonces, por curiosidad ... ¿Se las arregló para encontrar algún tipo de buena solución para esto?
ZeroOne
No No creo que lo que quiero sea posible en Java: /
Nathan Merrill

Respuestas:

1

OK, entonces las cosas se intensificaron y terminé con las siguientes diez clases ...

La conclusión de este método es que toda la comunicación ocurre usando la Messageclase, es decir, el juego nunca llama a los métodos de los jugadores directamente, sino que siempre usa una clase de comunicador desde su marco. Hay un comunicador basado en la reflexión para las clases nativas de Java y luego debe haber un comunicador personalizado para todos los jugadores que no sean Java. Message<Integer> message = new Message<>("say", Integer.class, "Hello");inicializaría un mensaje a un método nombrado saycon el parámetro que "Hello"devuelve un Integer. Esto luego se pasa a un comunicador (generado usando una fábrica basada en el tipo de jugador) que luego ejecuta el comando.

import java.util.Optional;

public class Game {
    Player player; // In reality you'd have a list here

    public Game() {
        System.out.println("Game starts");
        player = new PlayerOne();
    }

    public void play() {
        Message<Boolean> message1 = new Message<>("x", Boolean.class, true, false, true);
        Message<Integer> message2 = new Message<>("y", Integer.class, "Hello");
        Result result1 = sendMessage(player, message1);
        System.out.println("Response 1: " + result1.getResult());
        Result result2 = sendMessage(player, message2);
        System.out.println("Response 2: " + result2.getResult());
    }

    private Result sendMessage(Player player, Message<?> message1) {
        return Optional.ofNullable(player)
                .map(Game::createCommunicator)
                .map(comm -> comm.executeCommand(message1))
                .get();
    }

    public static void main(String[] args) {
        Game game = new Game();
        game.play();
    }

    private static PlayerCommunicator createCommunicator(Player player) {
        if (player instanceof NativePlayer) {
            return new NativePlayerCommunicator((NativePlayer) player);
        }
        return new ExternalPlayerCommunicator((ExternalPlayer) player);
    }
}

public abstract class Player {}

public class ExternalPlayer extends Player {}

public abstract class NativePlayer extends Player {
    abstract boolean x(Boolean a, Boolean b, Boolean c);
    abstract Integer y(String yParam);
    abstract Void z(Void zParam);
}

public abstract class PlayerCommunicator {
    public abstract Result executeCommand(Message message);
}

import java.lang.reflect.Method;
public class NativePlayerCommunicator extends PlayerCommunicator {
    private NativePlayer player;
    public NativePlayerCommunicator(NativePlayer player) { this.player = player; }
    public Result executeCommand(Message message) {
        try {
            Method method = player.getClass().getDeclaredMethod(message.getMethod(), message.getParamTypes());
            return new Result(method.invoke(player, message.getArguments()));
        } catch (Exception e) { throw new RuntimeException(e); }
    }
}

public class ExternalPlayerCommunicator extends PlayerCommunicator {
    private ExternalPlayer player;
    public ExternalPlayerCommunicator(ExternalPlayer player) { this.player = player; }
    @Override
    public Result executeCommand(Message message) { /* Do some IO stuff */ return null; }
}

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Message<OUT> {
    private final String method;
    private final Class<OUT> returnType;
    private final Object[] arguments;

    public Message(final String method, final Class<OUT> returnType, final Object... arguments) {
        this.method = method;
        this.returnType = returnType;
        this.arguments = arguments;
    }

    public String getMethod() { return method; }
    public Class<OUT> getReturnType() { return returnType; }
    public Object[] getArguments() { return arguments; }

    public Class[] getParamTypes() {
        List<Class> classes = Arrays.stream(arguments).map(Object::getClass).collect(Collectors.toList());
        Class[] classArray = Arrays.copyOf(classes.toArray(), classes.size(), Class[].class);
        return classArray;
    }
}

public class PlayerOne extends NativePlayer {
    @Override
    boolean x(Boolean a, Boolean b, Boolean c) {
        System.out.println(String.format("x called: %b %b %b", a, b, c));
        return a || b || c;
    }

    @Override
    Integer y(String yParam) {
        System.out.println("y called: " + yParam);
        return yParam.length();
    }

    @Override
    Void z(Void zParam) {
        System.out.println("z called");
        return null;
    }
}

public class Result {
    private final Object result;
    public Result(Object result) { this.result = result; }
    public Object getResult() { return result; }
}

(PD. Otras palabras clave en mi mente que no puedo refinar en algo útil en este momento: Patrón de comando , Patrón de visitante , java.lang.reflect.ParameterizedType )

Cero uno
fuente
Mi objetivo es evitar que se requiera que la persona que Playerescribió escriba PlayerComm. Si bien las interfaces del comunicador realizan la conversión automática para mí, todavía me encuentro con el mismo problema de tener que escribir la misma sendRequest()función en cada método.
Nathan Merrill
He reescrito mi respuesta. Sin embargo, ahora me doy cuenta de que usar el patrón de fachada podría ser el camino a seguir aquí, envolviendo entradas que no sean Java en lo que se ve exactamente como una entrada Java. Así que no pierdas el tiempo con algunos comunicadores o reflexiones.
ZeroOne
2
"Está bien, así que las cosas se intensificaron y terminé con las siguientes diez clases" Si tuviera un centavo ...
Jack
¡Ay, mis ojos! De todos modos, ¿podríamos obtener un diagrama de clase para ir con estas 10 clases? ¿O estás demasiado ocupado escribiendo tu respuesta de patrón de fachada?
candied_orange
@CandiedOrange, de hecho creo que ya he pasado suficiente tiempo con esta pregunta. Espero que alguien más dé su versión de una solución, posiblemente usando un patrón de fachada.
ZeroOne