Input/Output Implementierung eines CommandHandlers/Parsers für viele Eingaben

Diskutiere Implementierung eines CommandHandlers/Parsers für viele Eingaben im Allgemeine Java-Themen Bereich.

Bitte aktiviere JavaScript!
D

DagobertDuck

Hi,

ich arbeite momentan an einem Projekt zur Eisenbahnsimulierung und bin mir bei der Implementierung der Benutzerschnittstelle noch nicht sicher.
Bisher war meine Idee, ein Interface namens Command zu erstellen und dann für jeden Befehl eine eigene Klasse. Wäre es nicht eventuell sogar besser jeweils eine Klasse für alle Add Befehle (add track, add train, ...), für alle List Befehle (list tracks, list engines, ...) zu erstellen, oder für alle Befehle, die sich auf ein coach (Wagen) beziehen also z. B. CoachCommands (create coach, list coaches, ...)?

Könnte mir jemand eine sinnvolle Klassen bzw. Implementierungsstruktur geben? Was mich außerdem verunsichert, sind die Anzahl Wörter der Befehle. Beispielsweise gibt es Befehle, die aus zwei Wörtern bestehen sollen (add track, delete track, list tracks, ...) und wiederum gibt es Befehle, die nur aus einem Wort bestehen (step, exit, ...). Ich zerschlage mir schon den ganzen Tag den Kopf darüber und bastel mir Codeschnipsel zusammen, wie ich dies clever implementieren kann. Leider bisher ohne wirklichen Erfolg.

Hier ist die Liste aller Befehle, die ich implementieren möchte:
- add track <startpoint> -> <endpoint>
- delete track <trackID>
- list tracks
- set switch <trackID> position <point>
- create engine <engineType> <class> <name> <length> <couplingFront> <couplingBack>
- list engines
- create coach <coachType> <length> <couplingFront> <couplingBack>
- list coaches
- create train-set <class> <name> <length> <couplingFront> <couplingBack>
- list train-sets
- delete rolling stock <id>
- add train <trainID> <rollingStockID>
- list trains
- show train <trainID>
- put train <trainID> at <point> in direction <x>,<y>
- step <speed>
- exit
 
mihe7

mihe7

Zum Beispiel:
Java:
interface Command {
    public void execute(List<String> arguments);
}
Dann ein paar Helferlein
Java:
public class UnknownCommandException extends RuntimeException {
    private String token;

    public UnknownCommandException(String token) {
        super("Unbekannter Befehl " + token);
        this.token = token;
    }

    public String getToken() { return token; }
}
und
Java:
class DefaultCommand implements Command {
    private Map<String, Command> commands = new HashMap<>();

    public void register(String name, Command command) {
        commands.put(name, command);
    }

    @Override
    public void execute(List<String> arguments) {
        if (arguments.isEmpty()) { return; }

        Command cmd = commands.get(arguments.get(0));
        if (cmd != null) {
            cmd.execute(arguments.subList(1, arguments.size()));
        } else {
            throw new UnknownCommandException();
        }
    }    
}
Jetzt brauchen wir noch die Befehlszeile
Java:
class CommandLine {
    private final Command command;

    public CommandLine(Command root) {
        command = root;
    }

    public void execute(String line) {
        command.execute(Arrays.asList(line.split("\\s")));
    }
}
Und was zum Testen:
Java:
public class Test {
    private CommandLine cli;

    public Test() {
        DefaultCommand add = new DefaultCommand();
        add.register("track", args -> System.out.println("add track: " + args));
        add.register("train", args -> System.out.println("add train: " + args));

        DefaultCommand delete = new DefaultCommand();
        delete.register("track", args -> System.out.println("delete track: " + args));
        delete.register("train", args -> System.out.println("delete train: " + args));
        DefaultCommand root = new DefaultCommand();
        root.register("add", add);
        root.register("delete", delete);

        cli = new CommandLine(root);
    }

    public void execute(String line) {
        cli.execute(line);
    }

    public static void main(String[] args) {
        Test test = new Test();
        test.execute("add track a b c");
        test.execute("add train xy");
        test.execute("delete track 1");
        test.execute("xy");
    }
}
 
D

DagobertDuck

Kann mir niemand weiterhelfen? Ich bin schon fast am verzweifeln..
 
C

CSHW89

Seit Java 8 implementiere ich das Command-Pattern eher mit einem einzigen Funktionalen Interface und Lambdaausdrücke für jedes einzelne Command. Ggf. können die Command-Factory-Methoden in ein Controller umgelagert werden.
Java:
interface Command {
    public void execute();
    
    public static Command addTrack(Point start, Point end) {
        return () -> getTrackList().add(new Track(start, end));
    }
    public static Command removeTrack(Track track) {
        return () -> getTrackList().remove(track);
    }
    ...
}
 
D

DagobertDuck

Vielen Dank für die Hilfe @mihe7 und @CSHW89. Das hilft mir bereits enorm bei dem Verständnis der Implementierung..
Jedoch bin ich mir immer noch nicht ganz sicher wie ich nun weiter machen soll.
Ist für jeden Command eine eigene Klasse sinnvoll und wie würde ich das dann konkret in den Vorschlag von @mihe7 einarbeiten? Könnte mir jemand dies anhand eines beliebigen Befehls verdeutlichen?
 
D

DagobertDuck

@mihe7 Könntest du deine Antwort noch etwas ausführen, indem du dein Command-Pattern konkret auf einen meiner Befehle anwendest? Ich wäre dir sehr dankbar, da ich noch nicht ganz verstanden habe, wie ich alle Befehle genau implementieren soll.
 
mihe7

mihe7

indem du dein Command-Pattern konkret auf einen meiner Befehle anwendest?
Wie meinst Du das? Im Code sind bereits vier Befehle implementiert.

Die Implementierung des Command-Interfaces erfolgt einmal über die Klasse DefaultCommand (das ist ein Befehl, bei dem Unterbefehle registriert werden können) und einmal in Form von Lambda-Ausdrücken.

Das hier
Java:
add.register("track", args -> System.out.println("add track: " + args));
ist sozusagen die Kurzschreibweise für:
Java:
add.register("track", new Command() {
    @Override
    public void execute(List<String> args) {
        System.out.println("add track: " + args);
    }
});
wobei die anonyme innere Klasse eben den "add track"-Befehl implementiert.

Die Implementierung von Command hat nur die Aufgabe, die gegebenen Parameter in einen geeigneten Methodenaufruf umzusetzen. Dabei können (bzw. sollten) die "Syntax" überprüft und Parameter konvertiert werden.

Nehmen wir mal den Befehl "add track <startpoint> -> <endpoint>", dann würde die innere Klasse oben als Parameter-Liste ["<startpoint>", "->", "<endpoint>"] erhalten.

D. h. der Befehl "add track" erwartet die Parameter in der angegebenen Form. Außerdem nehmen wir mal an, dass startpoint und endpoint ganzzahlige Werte (int) sein sollen. Dann könntest Du schemenhaft schreiben:
Java:
public void execute(List<String> args) {
    if (args.size() != 3) { throw new IllegalArgumentException("falsche Anzahl an Parametern"); }
    if (!"->".equals(args.get(1))) { throw new IllegalArgumentException("zweiter Parameter muss '->' sein, war aber " + args.get(1)); }

    String startpoint = args.get(0);
    String endpoint = args.get(2);
    int start = Integer.parseInt(startpoint);
    int end = Integer.parseInt(endpoint);

    tracks.add(start, end); // oder was auch immer
}
 
D

DagobertDuck

Super, danke schön! Ich war noch so mit meiner ursprünglichen Implementierungs-Idee in Gedanken, dass ich gar nicht richtig bemerkt habe, dass deine Struktur alle Commands in einer Klasse hinzufügt (inklusiv der execute() Methoden der jeweiligen Befehle).
 
M

mrBrown

Wo kommen denn die Commands in String-Form her? uU kann man da ja schon ansetzen und es anders lösen.
 
D

DagobertDuck

Das Problem der Benutzerschnittstelle habe ich bereits gelöst. Ich habe mihe7's Code etwas angepasst und für jeden Command eine eigene Klasse erstellt. Im Java-Anfänger Forum habe ich eine Frage zum Regex gestellt, eventuell könntest du mir dabei weiterhelfen.
 
D

DagobertDuck

@mihe7 Wie könnte man am einfachsten für deinen Code eine Fehlermeldung ausgeben, wenn z. B. nur "add" eingegeben wird. "quit abcdefg" funktioniert z. B. auch, obwohl nur "quit" ohne weitere Argumente angenommen werden soll. Für "add track" wird bereits die Fehlermeldung "falsche Anzahl an Argumenten" ausgegeben, da du eine Abfrage in der execute() Methode hinzugefügt hast.
 
D

DagobertDuck

@mihe7 Super, danke. Jetzt habe ich alles dementsprechend angepasst.
Der Übersicht halber habe ich alle Commands außer den "quit" Command in eigene Klassen ausgelagert.
Hast du eine Idee, wie ich auch den "quit" Command in eine eigene Klasse auslagern könnte?

Hier ist ein Ausschnitt meiner Main Klasse, sodass du verstehst, wie sie in etwa funktioniert:
Java:
while (running) {

        ...   

        test.execute(input);

        ...
     
    }

}
und hier ist mein derzeitiger "quit" Command:
Java:
root.register("quit", new DefaultCommand() {

    @Override

    public void execute(List<String> args) throws BadSyntaxException {

        if (!args.isEmpty()) { throw new BadSyntaxException("unknown command"); }

        running = false;

    }

});
Kann ich irgendwie in einer separaten Klasse den boolean running auf false setzen? Ich fände es schöner, wenn alle Befehle (inklusive des quit-Befehls) in einer eigenen Klasse sind.
 
mihe7

mihe7

Sicher, Du brauchst für das boolean nur ein veränderliches Objekt statt des primitiven Typs zu verwenden, z. B.
Java:
private final AtomicBoolean running = new AtomicBoolean(true);

// irgendwo
root.register("quit", args -> running.set(false));

// die loop
public void run() {
    while (runining.get()) {
        ...
    }
}
 
D

DagobertDuck

Danke, nur leider darf ich AtomicBoolean nicht verwenden. Nur Klassen aus java.util sind erlaubt. Gibt es eine Alternative?
 
D

DagobertDuck

@mihe7 Des Weiteren habe ich noch ein Problem mit dem Command
delete.register("rolling stock", new DeleteRollingStockCommand(register));. Da hier ein whitespace vorhanden ist, wird immer "unknown command" zurückgegeben. Jetzt möchte ich allerdings nicht noch einen Unterbefehl erstellen, also den rolling stock Befehl in "rolling" und in "stock" aufteilen. Hast du eine Idee, wie ich dies einfach lösen kann?
 
mihe7

mihe7

Gibt es eine Alternative?
Schreib einfach eine ähnliche Klasse.

Da hier ein whitespace vorhanden ist, wird immer "unknown command" zurückgegeben. Jetzt möchte ich allerdings nicht noch einen Unterbefehl erstellen, also den rolling stock Befehl in "rolling" und in "stock" aufteilen. Hast du eine Idee, wie ich dies einfach lösen kann?
rolling ist ein DefaultCommand, bei dem Du den Unterbefehl stock registrierst.

Das gleiche gilt für die Befehle "show train" und "put train".
So ist es.
 
D

DagobertDuck

@mihe7 In dem Sinne ist „stock“ kein Unterbefehl und macht nur in Verbindung mit „rolling“ Sinn, insgesamt also „rolling stock“. Rolling stock = Rollmaterial (~Zugteile). Wenn es hierfür keine einfache Lösung gibt werde ich das allerdings so machen.
 
M

mrBrown

@mihe7 In dem Sinne ist „stock“ kein Unterbefehl und macht nur in Verbindung mit „rolling“ Sinn, insgesamt also „rolling stock“. Rolling stock = Rollmaterial (~Zugteile). Wenn es hierfür keine einfache Lösung gibt werde ich das allerdings so machen.
Die einfachste Lösung wäre wohl ein Bindestrich statt Leerzeichen, wie auch bei train-set/train-sets
 
mihe7

mihe7

In dem Sinne ist „stock“ kein Unterbefehl
Das spielt keine Rolle. Im Modell kann ein Befehl per Definition keine Leerzeichen enthalten. Die Frage ist also nur, ob das Modell geeignet ist, Deine Befehle abzubilden. Und das funktioniert für die genannten Fälle (noch). Tatsächlich ist das hier auch eher eine Frage der Begrifflichkeit: verwende statt "Unterbefehl" einfach "Teil eines Befehls" :)
 
D

DagobertDuck

Sicher, Du brauchst für das boolean nur ein veränderliches Objekt statt des primitiven Typs zu verwenden, z. B.
Java:
private final AtomicBoolean running = new AtomicBoolean(true);

// irgendwo
root.register("quit", args -> running.set(false));

// die loop
public void run() {
    while (runining.get()) {
        ...
    }
}
So sieht jetzt meine Implementierung bisher aus:

Main.java:
Java:
public class Main {

    public static void main(String[] args) {

        Session session = new Session();

        session.run();

    }

}
Session.java:
Java:
public class Session {

    protected boolean running = true;



    /**

     * After starting the session this method remains in a loop until the {@link this#terminate()}

     * method is called.

     */

    void run() {

        RailRegister register = new RailRegister();



        while (running) {

            String input = Terminal.readLine();

            try {

                UserInterface userInterface = new UserInterface(register);

                userInterface.execute(input);

            } catch (UnknownCommandException | BadSyntaxException e) {

                Terminal.printError(e.getMessage());

            }

        }

    }

    /**

     * Terminates the session.

     */

    public void terminate() {

        running = false;

    }

}
QuitCommand.java:
Java:
public class QuitCommand extends AbstractCommand {
    public QuitCommand(RailRegister register) {
        super(register);
    }

    @Override
    public void execute(List<String> arguments) throws BadSyntaxException {
        // TODO: Execute terminate() method
    }
}
Jedoch weiß ich noch nicht, wie ich in QuitCommand.java auf die terminate() Methode bzw. den running boolean zugreifen soll.
 
M

mrBrown

@mrBrown In diesem Fall leider schon :confused:.
Der „Hinweis“ kam deshalb, weil ich mir gut vorstellen kann, dass es einfach ein Fehler in der Aufgabenstellung/ein nicht bedachter Edge-Case ist, der eigentlich gar nicht so sein sollte - grad auch weil dieser Befehl sich in der Schreib-Art von allen anderen unterscheidet. Solche Fehler passieren häufig, und fallen einfach niemandem auf, wenn niemand nachfragt.
(Nur meine Meinung als der, der dabei meist auf der Aufgabensteller-Seite sitzt)
 
Thema: 

Implementierung eines CommandHandlers/Parsers für viele Eingaben

Passende Stellenanzeigen aus deiner Region:
Anzeige

Neue Themen

Anzeige

Anzeige
Oben