Command pattern w praktyce

TL;DR: Command pattern zamienia żądania w obiekty, dzięki czemu możesz je kolejkować, cofać (undo) i logować. Idealny do GUI, makr i systemów z historią operacji.

Wyobraź sobie pilota do telewizora. Każdy przycisk to komenda – włącz, wyłącz, zmień kanał. Pilot nie wie jak telewizor działa w środku, ale wie jakie komendy może wysłać. Command pattern działa podobnie – oddziela obiekt wysyłający żądanie od obiektu, który je wykonuje.

Dlaczego Command pattern jest ważny

W dzisiejszych aplikacjach użytkownicy oczekują funkcji undo/redo, makr i możliwości powtarzania operacji. Command pattern to fundament takich funkcjonalności. Bez niego kod szybko staje się chaotyczny, a dodanie nowych operacji wymaga zmian w wielu miejscach.

Co się nauczysz:

  • Jak implementować Command pattern w Javie
  • Kiedy używać tego wzorca w prawdziwych projektach
  • Jak zbudować system undo/redo
  • Praktyczne przykłady z GUI i aplikacji biznesowych
  • Najczęstsze błędy przy implementacji
Wymagania wstępne: Podstawy Java, znajomość interfejsów i dziedziczenia, podstawy wzorców projektowych

Podstawowa struktura Command pattern

Command pattern składa się z kilku kluczowych elementów:

Command: interfejs definiujący metodę execute()
ConcreteCommand: konkretna implementacja komendy
Receiver: obiekt, który faktycznie wykonuje operację
Invoker: obiekt, który wywołuje komendę
// Podstawowy interfejs Command
public interface Command {
    void execute();
}

// Receiver - obiekt wykonujący faktyczną pracę
public class Light {
    private boolean isOn = false;
    
    public void turnOn() {
        isOn = true;
        System.out.println("Światło włączone");
    }
    
    public void turnOff() {
        isOn = false;
        System.out.println("Światło wyłączone");
    }
    
    public boolean isOn() {
        return isOn;
    }
}

// Konkretne komendy
public class LightOnCommand implements Command {
    private Light light;
    
    public LightOnCommand(Light light) {
        this.light = light;
    }
    
    @Override
    public void execute() {
        light.turnOn();
    }
}

public class LightOffCommand implements Command {
    private Light light;
    
    public LightOffCommand(Light light) {
        this.light = light;
    }
    
    @Override
    public void execute() {
        light.turnOff();
    }
}

Prosty przykład – pilot zdalnego sterowania

// Invoker - pilot
public class RemoteControl {
    private Command[] commands;
    
    public RemoteControl(int numberOfSlots) {
        commands = new Command[numberOfSlots];
        
        // Inicjalizacja z pustymi komendami
        Command noCommand = () -> {}; // Lambda expression
        for (int i = 0; i < numberOfSlots; i++) {
            commands[i] = noCommand;
        }
    }
    
    public void setCommand(int slot, Command command) {
        commands[slot] = command;
    }
    
    public void buttonPressed(int slot) {
        commands[slot].execute();
    }
}

// Użycie
public class RemoteControlDemo {
    public static void main(String[] args) {
        // Tworzymy urządzenia (Receivers)
        Light livingRoomLight = new Light();
        Light kitchenLight = new Light();
        
        // Tworzymy komendy
        LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight);
        LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight);
        LightOnCommand kitchenLightOn = new LightOnCommand(kitchenLight);
        LightOffCommand kitchenLightOff = new LightOffCommand(kitchenLight);
        
        // Konfigurujemy pilot
        RemoteControl remote = new RemoteControl(4);
        remote.setCommand(0, livingRoomLightOn);
        remote.setCommand(1, livingRoomLightOff);
        remote.setCommand(2, kitchenLightOn);
        remote.setCommand(3, kitchenLightOff);
        
        // Używamy pilota
        remote.buttonPressed(0); // Włącza światło w salonie
        remote.buttonPressed(1); // Wyłącza światło w salonie
        remote.buttonPressed(2); // Włącza światło w kuchni
    }
}
Pro tip: Użycie lambda expressions w Java 8 znacznie upraszcza implementację prostych komend. Zamiast tworzenia osobnych klas, możesz pisać () -> receiver.action().

Dodanie funkcji Undo

Prawdziwa siła Command pattern ujawnia się przy implementacji funkcji cofania operacji:

// Rozszerzony interfejs z możliwością cofania
public interface UndoableCommand extends Command {
    void undo();
}

// Komenda z undo dla światła
public class LightCommand implements UndoableCommand {
    private Light light;
    private boolean previousState;
    
    public LightCommand(Light light) {
        this.light = light;
    }
    
    @Override
    public void execute() {
        previousState = light.isOn(); // Zapamiętaj stan przed wykonaniem
        if (previousState) {
            light.turnOff();
        } else {
            light.turnOn();
        }
    }
    
    @Override
    public void undo() {
        if (previousState) {
            light.turnOn(); // Przywróć poprzedni stan
        } else {
            light.turnOff();
        }
    }
}

// Pilot z funkcją undo
public class RemoteControlWithUndo {
    private UndoableCommand[] commands;
    private UndoableCommand lastCommand;
    
    public RemoteControlWithUndo(int numberOfSlots) {
        commands = new UndoableCommand[numberOfSlots];
        
        // Pusta komenda która nic nie robi
        UndoableCommand noCommand = new UndoableCommand() {
            public void execute() {}
            public void undo() {}
        };
        
        for (int i = 0; i < numberOfSlots; i++) {
            commands[i] = noCommand;
        }
        lastCommand = noCommand;
    }
    
    public void setCommand(int slot, UndoableCommand command) {
        commands[slot] = command;
    }
    
    public void buttonPressed(int slot) {
        commands[slot].execute();
        lastCommand = commands[slot];
    }
    
    public void undoButtonPressed() {
        lastCommand.undo();
    }
}
Kluczem do implementacji undo jest zapamiętanie stanu przed wykonaniem operacji. Każda komenda musi wiedzieć jak cofnąć swoje działanie.

Praktyczny przykład - edytor tekstu

// Prosty edytor tekstu
public class TextEditor {
    private StringBuilder content = new StringBuilder();
    
    public void write(String text) {
        content.append(text);
    }
    
    public void delete(int length) {
        int start = Math.max(0, content.length() - length);
        content.delete(start, content.length());
    }
    
    public String getContent() {
        return content.toString();
    }
    
    public void setContent(String newContent) {
        content = new StringBuilder(newContent);
    }
}

// Komenda pisania tekstu
public class WriteCommand implements UndoableCommand {
    private TextEditor editor;
    private String textToWrite;
    
    public WriteCommand(TextEditor editor, String text) {
        this.editor = editor;
        this.textToWrite = text;
    }
    
    @Override
    public void execute() {
        editor.write(textToWrite);
    }
    
    @Override
    public void undo() {
        editor.delete(textToWrite.length());
    }
}

// Komenda usuwania tekstu
public class DeleteCommand implements UndoableCommand {
    private TextEditor editor;
    private int lengthToDelete;
    private String deletedText;
    
    public DeleteCommand(TextEditor editor, int length) {
        this.editor = editor;
        this.lengthToDelete = length;
    }
    
    @Override
    public void execute() {
        String content = editor.getContent();
        int start = Math.max(0, content.length() - lengthToDelete);
        deletedText = content.substring(start); // Zapamiętaj usunięty tekst
        editor.delete(lengthToDelete);
    }
    
    @Override
    public void undo() {
        editor.write(deletedText); // Przywróć usunięty tekst
    }
}

// Menedżer historii dla pełnego undo/redo
public class CommandHistory {
    private Stack undoStack = new Stack<>();
    private Stack redoStack = new Stack<>();
    
    public void executeCommand(UndoableCommand command) {
        command.execute();
        undoStack.push(command);
        redoStack.clear(); // Nowe polecenie anuluje możliwość redo
    }
    
    public void undo() {
        if (!undoStack.isEmpty()) {
            UndoableCommand command = undoStack.pop();
            command.undo();
            redoStack.push(command);
        }
    }
    
    public void redo() {
        if (!redoStack.isEmpty()) {
            UndoableCommand command = redoStack.pop();
            command.execute();
            undoStack.push(command);
        }
    }
}
Pułapka: Przy implementacji undo/redo pamiętaj o czyszczeniu stosu redo po wykonaniu nowej komendy. Inaczej użytkownik może cofnąć się do nieistniejącego stanu!

Makro komendy - grupowanie operacji

// Makro komenda grupująca kilka operacji
public class MacroCommand implements UndoableCommand {
    private UndoableCommand[] commands;
    
    public MacroCommand(UndoableCommand[] commands) {
        this.commands = commands;
    }
    
    @Override
    public void execute() {
        for (UndoableCommand command : commands) {
            command.execute();
        }
    }
    
    @Override
    public void undo() {
        // Cofaj operacje w odwrotnej kolejności!
        for (int i = commands.length - 1; i >= 0; i--) {
            commands[i].undo();
        }
    }
}

// Przykład użycia - makro "Dobranoc"
public class GoodnightMacroExample {
    public static void main(String[] args) {
        Light livingRoomLight = new Light();
        Light kitchenLight = new Light();
        Light bedroomLight = new Light();
        
        // Tworzenie makra "Dobranoc" - wyłącz wszystkie światła
        UndoableCommand[] goodnightCommands = {
            new LightOffCommand(livingRoomLight),
            new LightOffCommand(kitchenLight), 
            new LightOffCommand(bedroomLight)
        };
        
        MacroCommand goodnightMacro = new MacroCommand(goodnightCommands);
        
        // Wykonaj makro
        goodnightMacro.execute(); // Wyłącza wszystkie światła
        
        // Ups, jednak jeszcze nie śpię - cofnij makro
        goodnightMacro.undo(); // Włącza wszystkie światła z powrotem
    }
}

Zastosowania w prawdziwych projektach

Command pattern to jak lista zadań do wykonania. Możesz zadania dodawać, usuwać, grupować i wykonywać w dowolnej kolejności. To samo robi Command pattern z operacjami w kodzie.

### Popularne zastosowania:
- **GUI aplikacje** - przyciski, menu, skróty klawiszowe
- **Edytory** - undo/redo, makra, automatyzacja
- **Gry** - system komend, replay, AI scripting
- **Web aplikacje** - kolejkowanie zadań, batch operations
- **Enterprise systems** - audit log, transaction management

Kiedy używać Command pattern zamiast zwykłych metod?

Gdy potrzebujesz: undo/redo, kolejkowania operacji, logowania działań, makr, lub gdy chcesz oddzielić obiekt wywołujący od obiektu wykonującego operację.

Czy Command pattern spowalnia aplikację?

Minimalnie. Overhead to dodatkowy obiekt na operację, ale korzyści (elastyczność, testowalność) zwykle przeważają. W krytycznych miejscach można użyć object pooling.

Jak testować komendy?

Mocki są idealne. Możesz sprawdzić czy komenda wywołała odpowiednie metody na receiver'ze, w odpowiedniej kolejności, z poprawnymi parametrami.

Co z pamięcią przy wielu komendach undo?

Ogranicz stos undo (np. do 50 operacji) lub implementuj snapshot pattern dla dużych zmian stanu. Możesz też serializować stare komendy na dysk.

Czy można łączyć Command z innymi wzorcami?

Tak! Często łączy się z: Composite (makro komendy), Memento (snapshots), Observer (powiadomienia o zmianach), Queue (kolejkowanie zadań).

Przydatne zasoby:

🚀 Zadanie dla Ciebie

Mini Kalkulator Challenge: Stwórz prostą aplikację kalkulatora używając Command pattern. Zaimplementuj operacje dodawania, odejmowania, mnożenia i dzielenia z pełną funkcjonalnością undo/redo. Dodaj makro komendę do wykonywania serii obliczeń. Bonus: zrób GUI w Swing/JavaFX z przyciskami i historią operacji!

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry