Kihagyás

Command (Parancs)

Kategória: Viselkedési minta (Behavioral Pattern)

Lényege és célja

A Command minta egy kérést (műveletet) önálló objektummá alakít. Ez az objektum mindent magában hordoz, ami a művelet végrehajtásához kell: a címzettet (akit hív), a paramétereket, és gyakran a visszacsinálás (undo) logikáját is.

A művelet ezzel ugyanolyan értékké válik, mint bármi más: el lehet tárolni, sorba lehet rakni, naplózni, hálózaton átküldeni, vagy egy stack-en visszafelé lejátszani.

Miért zseniális ez a minta?

Egy szövegszerkesztős példán könnyű megérteni:

  1. Undo/Redo ingyen jár: ha minden művelet (AppendCommand, DeleteCommand) tudja önmagát visszacsinálni, a CommandHistory csak egy Stack-en tárolja őket, és visszafelé hívja az undo()-t.
  2. A hívó és a végrehajtó szétválik: a CommandHistory (Invoker) nem tudja, mit csinál egy konkrét parancs – csak execute()-t hív. Ez ideális makrók, ütemezett feladatok, sorba állítás vagy event sourcing esetén.
  3. Új művelet bevezetése = új osztály: nem kell hozzányúlni a TextEditor-hoz vagy a History-hoz, csak írunk egy új Command implementációt (Open/Closed).

Mikor használjuk?

  • Amikor undo/redo, makró-rögzítés, vagy műveletnaplózás (audit log) szükséges.
  • Amikor műveleteket sorba kell tenni, ütemezni, vagy másik szálon/szerveren végrehajtani (job queue, task scheduler).
  • Amikor a felhasználói felület gombjait (GUI), menüpontjait, billentyűparancsait egységesen, paraméteres objektumként akarjuk a háttérlogikához kötni.

Mermaid Diagram

A diagramon a négy klasszikus szereplő látható: Command interfész, konkrét parancsok (AppendCommand, DeleteCommand), a Receiver (TextEditor) aki a tényleges munkát végzi, és az Invoker (CommandHistory) ami a parancsokat tárolja és lejátssza.

classDiagram
    class Command {
        <<interface>>
        +execute() void
        +undo() void
    }

    class AppendCommand {
        -textEditor: TextEditor
        -text: String
        +execute() void
        +undo() void
    }

    class DeleteCommand {
        -textEditor: TextEditor
        -count: int
        -text: String
        +execute() void
        +undo() void
    }

    class TextEditor {
        -content: StringBuilder
        +append(text: String) void
        +deleteLast(count: int) String
    }

    class CommandHistory {
        -commands: Stack~Command~
        +executeCommand(c: Command) void
        +undo() void
    }

    class Client

    AppendCommand ..|> Command : "implementálja"
    DeleteCommand ..|> Command : "implementálja"

    %% A konkrét parancsok ismerik a Receivert (TextEditor)
    AppendCommand --> TextEditor : "delegál"
    DeleteCommand --> TextEditor : "delegál"

    %% Invoker csak a Command interfészt ismeri
    CommandHistory o--> Command : "tárolja"

    Client --> CommandHistory : "használja"
    Client --> TextEditor : "példányosítja"

Forráskód

package behavioral.command;

import java.util.Objects;
import java.util.Stack;

interface Command {
    void execute();
    void undo();
}

class TextEditor {

    private final StringBuilder content = new StringBuilder();

    void append(String text) {
        content.append(text);
    }

    String deleteLast(int count) {
        var text = content.toString();
        content.delete(content.length() - count, content.length());
        return text.substring(text.length() - count);
    }

    String getText() {
        return content.toString();
    }
}

class AppendCommand implements Command {

    private final TextEditor textEditor;
    private final String text;

    public AppendCommand(TextEditor textEditor, String text) {
        this.textEditor = textEditor;
        this.text = text;
    }

    @Override
    public void execute() {
        textEditor.append(text);
    }

    @Override
    public void undo() {
        textEditor.deleteLast(text.length());
    }
}

class DeleteCommand implements Command {

    private final TextEditor textEditor;
    private String text;
    private final int count;

    public DeleteCommand(TextEditor textEditor, int count) {
        this.textEditor = textEditor;
        this.count = count;
    }

    @Override
    public void execute() {
        text = textEditor.deleteLast(count);

    }

    @Override
    public void undo() {
        if (Objects.nonNull(text)) {
            textEditor.append(text);
        }
    }
}

class CommandHistory {

    private final Stack<Command> commands = new Stack<>();

    void executeCommand(Command command) {
        command.execute();
        commands.add(command);
    }

    void undo() {
        if(!commands.isEmpty()) {
            commands.pop().undo();
        }
    }
}

class Main {
    static void main() {
        final var textEditor = new TextEditor();

        final var history = new CommandHistory();
        history.executeCommand(new AppendCommand(textEditor, "Hello "));
        history.executeCommand(new AppendCommand(textEditor, "World!"));
        history.executeCommand(new DeleteCommand(textEditor, 6));
        history.undo();
        history.undo();
        history.executeCommand(new AppendCommand(textEditor, "World!"));

        System.out.println(textEditor.getText());
    }
}