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
Podstawowa struktura Command pattern
Command pattern składa się z kilku kluczowych elementów:
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 } }
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(); } }
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 StackundoStack = 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); } } }
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
### 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
Gdy potrzebujesz: undo/redo, kolejkowania operacji, logowania działań, makr, lub gdy chcesz oddzielić obiekt wywołujący od obiektu wykonującego operację.
Minimalnie. Overhead to dodatkowy obiekt na operację, ale korzyści (elastyczność, testowalność) zwykle przeważają. W krytycznych miejscach można użyć object pooling.
Mocki są idealne. Możesz sprawdzić czy komenda wywołała odpowiednie metody na receiver'ze, w odpowiedniej kolejności, z poprawnymi parametrami.
Ogranicz stos undo (np. do 50 operacji) lub implementuj snapshot pattern dla dużych zmian stanu. Możesz też serializować stare komendy na dysk.
Tak! Często łączy się z: Composite (makro komendy), Memento (snapshots), Observer (powiadomienia o zmianach), Queue (kolejkowanie zadań).
Przydatne zasoby:
- Oracle Documentation - Runnable Interface
- Refactoring Guru - Command Pattern
- Java Design Patterns - Command Examples
🚀 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!