Code Design: Delegation beliebiger Funktionen

9

Bei PPCG gibt es häufig King of the Hill- Herausforderungen, bei denen verschiedene Code-Bots gegeneinander antreten. Wir möchten diese Herausforderungen nicht auf eine einzige Sprache beschränken, daher führen wir plattformübergreifende Kommunikation über Standard-E / A durch.

Mein Ziel ist es, ein Framework zu schreiben, mit dem Herausforderer diese Herausforderungen leichter schreiben können. Ich habe mir folgende Anforderungen ausgedacht, die ich erfüllen möchte:

  1. Der Herausforderungsschreiber kann eine Klasse erstellen, in der Methoden jede der verschiedenen Kommunikationen darstellen . Zum Beispiel würde der Autor bei unserer Herausforderung Gut gegen Böse eine PlayerKlasse erstellen, die eine abstract boolean vote(List<List<Boolean>> history)Methode enthält.

  2. Die Steuerung kann Instanzen der obigen Klasse bereitstellen, die über Standard-E / A kommunizieren, wenn die oben genannten Methoden aufgerufen werden . Allerdings kommunizieren nicht alle Instanzen der oben genannten Klasse notwendigerweise über Standard-E / A. 3 der Bots können native Java-Bots sein (die einfach die PlayerKlasse überschreiben , wobei weitere 2 in einer anderen Sprache vorliegen).

  3. Die Methoden haben nicht immer die gleiche Anzahl von Argumenten (noch haben sie immer einen Rückgabewert)

  4. Ich möchte, dass der Herausforderer so wenig wie möglich arbeiten muss, um mit meinem Framework zu arbeiten.

Ich bin nicht dagegen, Reflexion zu verwenden, um diese Probleme zu lösen. Ich habe darüber nachgedacht, vom Herausforderungsschreiber Folgendes zu verlangen:

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

Wenn es jedoch mehrere Methoden gibt, kann sich dies ziemlich wiederholen, und das ständige Casting macht keinen Spaß. ( sendMessageIn diesem Beispiel würde eine variable Anzahl von ObjectArgumenten akzeptiert und ein zurückgegeben. Object)

Gibt es einen besseren Weg, dies zu tun?

Nathan Merrill
quelle
1
Ich bin verwirrt über den " PlayerComm extends Player" -Teil. Erweitern sich alle Java-Teilnehmer Player, und diese PlayerCommKlasse ist ein Adapter für Nicht-Java-Teilnehmer?
ZeroOne
Ja, das ist richtig
Nathan Merrill
Also aus Neugier ... Haben Sie es geschafft, eine nette Lösung dafür zu finden?
ZeroOne
Nee. Ich glaube nicht, dass das, was ich will, in Java möglich ist: /
Nathan Merrill

Antworten:

1

OK, die Dinge eskalierten und ich bekam die folgenden zehn Klassen ...

Das Fazit dieser Methode ist, dass die gesamte Kommunikation über die MessageKlasse erfolgt, dh das Spiel ruft niemals die Methoden der Spieler direkt auf, sondern verwendet immer eine Kommunikatorklasse aus Ihrem Framework. Es gibt einen reflexionsbasierten Kommunikator für native Java-Klassen und dann muss es einen benutzerdefinierten Kommunikator für alle Nicht-Java-Player geben. Message<Integer> message = new Message<>("say", Integer.class, "Hello");würde eine Nachricht mit einer Methode initialisieren, deren Name einen sayParameter "Hello"zurückgibt Integer. Dies wird dann an einen Kommunikator übergeben (der mithilfe einer Factory basierend auf dem Spielertyp generiert wurde), der dann den Befehl ausführt.

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

(PS. Andere Schlüsselwörter in meinem Kopf, die ich momentan nicht ganz verfeinern kann: Befehlsmuster , Besuchermuster , java.lang.reflect.ParameterizedType )

Null eins
quelle
Mein Ziel ist es zu verhindern, dass die Person, die gemacht hat, überhaupt Playerschreibt PlayerComm. Während die Communicator-Schnittstellen für mich ein automatisches Casting durchführen, habe ich immer noch das gleiche Problem, dass ich sendRequest()in jeder Methode dieselbe Funktion schreiben muss .
Nathan Merrill
Ich habe meine Antwort umgeschrieben. Jetzt ist mir jedoch klar, dass die Verwendung des Fassadenmusters tatsächlich der richtige Weg ist, indem Nicht-Java-Einträge in etwas eingewickelt werden, das genau wie ein Java-Eintrag aussieht. Also kein Herumalbern mit ein paar Kommunikatoren oder Reflexionen.
ZeroOne
2
"OK, die Dinge eskalierten irgendwie und ich endete mit den folgenden zehn Klassen." Wenn ich einen Nickel hätte ...
Jack
Au, meine Augen! Wie auch immer, wir könnten ein Klassendiagramm für diese 10 Klassen erstellen? Oder sind Sie zu beschäftigt damit, Ihre Antwort auf das Fassadenmuster zu schreiben?
candied_orange
@CandiedOrange, tatsächlich denke ich, dass ich bereits genug Zeit mit dieser Frage verbracht habe. Ich hoffe, dass jemand anderes seine Version einer Lösung gibt, möglicherweise unter Verwendung eines Fassadenmusters.
ZeroOne