Command Pattern

1. Giới Thiệu

1.1 Định Nghĩa

Command Pattern là một behavioral pattern cho phép đóng gói một yêu cầu dưới dạng một đối tượng, từ đó có thể lưu trữ các yêu cầu trong hàng đợi, log lại lịch sử thực thi và hỗ trợ hoàn tác các thao tác.

1.2 Mục Đích

  • Tách biệt đối tượng gửi yêu cầu và đối tượng thực hiện yêu cầu
  • Đóng gói yêu cầu thành đối tượng
  • Hỗ trợ undo/redo operations
  • Hỗ trợ queuing và logging operations
  • Tăng tính mở rộng của hệ thống

2. Cấu Trúc Cơ Bản

2.1 Text Editor Example

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

// Receiver
public class TextEditor {
    private StringBuilder content;

    public TextEditor() {
        this.content = new StringBuilder();
    }

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

    public void deleteText(int length) {
        content.delete(
            content.length() - length, 
            content.length());
    }

    public String getContent() {
        return content.toString();
    }
}

// Concrete Commands
public class InsertTextCommand implements Command {
    private TextEditor editor;
    private String text;

    public InsertTextCommand(
            TextEditor editor, String text) {
        this.editor = editor;
        this.text = text;
    }

    @Override
    public void execute() {
        editor.insertText(text);
    }

    @Override
    public void undo() {
        editor.deleteText(text.length());
    }
}

public class DeleteTextCommand implements Command {
    private TextEditor editor;
    private String deletedText;
    private int length;

    public DeleteTextCommand(
            TextEditor editor, int length) {
        this.editor = editor;
        this.length = length;
    }

    @Override
    public void execute() {
        deletedText = editor.getContent().substring(
            editor.getContent().length() - length);
        editor.deleteText(length);
    }

    @Override
    public void undo() {
        editor.insertText(deletedText);
    }
}

// Invoker
public class EditorInvoker {
    private Stack<Command> undoStack = new Stack<>();
    private Stack<Command> redoStack = new Stack<>();

    public void executeCommand(Command command) {
        command.execute();
        undoStack.push(command);
        redoStack.clear();
    }

    public void undo() {
        if (!undoStack.isEmpty()) {
            Command command = undoStack.pop();
            command.undo();
            redoStack.push(command);
        }
    }

    public void redo() {
        if (!redoStack.isEmpty()) {
            Command command = redoStack.pop();
            command.execute();
            undoStack.push(command);
        }
    }
}

3. Ví Dụ Thực Tế

3.1 Order Processing System

// Command interface
public interface OrderCommand {
    void execute();
    void undo();
    OrderStatus getStatus();
}

// Receiver
public class Order {
    private String orderId;
    private OrderStatus status;
    private List<OrderItem> items;

    public void process() {
        // Process order
        status = OrderStatus.PROCESSING;
    }

    public void cancel() {
        // Cancel order
        status = OrderStatus.CANCELLED;
    }

    public void ship() {
        // Ship order
        status = OrderStatus.SHIPPED;
    }

    // Getters and setters
}

// Concrete Commands
public class ProcessOrderCommand 
        implements OrderCommand {
    private Order order;
    private OrderRepository repository;

    public ProcessOrderCommand(
            Order order, 
            OrderRepository repository) {
        this.order = order;
        this.repository = repository;
    }

    @Override
    public void execute() {
        order.process();
        repository.save(order);
    }

    @Override
    public void undo() {
        order.cancel();
        repository.save(order);
    }

    @Override
    public OrderStatus getStatus() {
        return order.getStatus();
    }
}

public class ShipOrderCommand 
        implements OrderCommand {
    private Order order;
    private ShippingService shippingService;
    private OrderRepository repository;

    @Override
    public void execute() {
        shippingService.createShipment(order);
        order.ship();
        repository.save(order);
    }

    @Override
    public void undo() {
        shippingService.cancelShipment(order);
        order.process();
        repository.save(order);
    }

    @Override
    public OrderStatus getStatus() {
        return order.getStatus();
    }
}

3.2 Task Scheduler

// Command interface
public interface Task extends Command {
    String getId();
    LocalDateTime getScheduledTime();
    boolean isRecurring();
}

// Concrete Commands
public class EmailTask implements Task {
    private String id;
    private String recipient;
    private String subject;
    private String content;
    private LocalDateTime scheduledTime;
    private EmailService emailService;

    @Override
    public void execute() {
        emailService.sendEmail(
            recipient, subject, content);
    }

    @Override
    public void undo() {
        emailService.sendCancellation(
            recipient, subject);
    }

    @Override
    public String getId() {
        return id;
    }

    @Override
    public LocalDateTime getScheduledTime() {
        return scheduledTime;
    }

    @Override
    public boolean isRecurring() {
        return false;
    }
}

// Scheduler
public class TaskScheduler {
    private PriorityQueue<Task> taskQueue;
    private Map<String, Task> taskMap;
    private ScheduledExecutorService executor;

    public TaskScheduler() {
        this.taskQueue = new PriorityQueue<>(
            Comparator.comparing(Task::getScheduledTime));
        this.taskMap = new ConcurrentHashMap<>();
        this.executor = Executors
            .newScheduledThreadPool(5);
    }

    public void scheduleTask(Task task) {
        taskMap.put(task.getId(), task);
        taskQueue.offer(task);

        long delay = ChronoUnit.MILLIS.between(
            LocalDateTime.now(), 
            task.getScheduledTime());

        executor.schedule(
            () -> executeTask(task),
            delay,
            TimeUnit.MILLISECONDS);
    }

    private void executeTask(Task task) {
        try {
            task.execute();
            if (task.isRecurring()) {
                rescheduleTask(task);
            } else {
                taskMap.remove(task.getId());
            }
        } catch (Exception e) {
            handleTaskError(task, e);
        }
    }

    private void rescheduleTask(Task task) {
        // Reschedule logic for recurring tasks
    }

    private void handleTaskError(
            Task task, Exception e) {
        // Error handling logic
    }
}

4. Spring Framework Integration

4.1 Configuration

@Configuration
public class CommandConfig {
    @Bean
    public CommandInvoker commandInvoker() {
        return new CommandInvoker();
    }

    @Bean
    public TaskScheduler taskScheduler() {
        return new TaskScheduler();
    }
}

4.2 Implementation

@Service
public class OrderService {
    private final CommandInvoker invoker;
    private final OrderRepository repository;
    private final ShippingService shippingService;

    public OrderResult processOrder(Order order) {
        try {
            OrderCommand command = new ProcessOrderCommand(
                order, repository);
            invoker.executeCommand(command);
            return OrderResult.success();
        } catch (Exception e) {
            return OrderResult.failure(e.getMessage());
        }
    }

    public OrderResult shipOrder(Order order) {
        try {
            OrderCommand command = new ShipOrderCommand(
                order, shippingService, repository);
            invoker.executeCommand(command);
            return OrderResult.success();
        } catch (Exception e) {
            return OrderResult.failure(e.getMessage());
        }
    }

    public void undoLastOperation() {
        invoker.undo();
    }
}

@RestController
@RequestMapping("/api/orders")
public class OrderController {
    private final OrderService orderService;

    @PostMapping("/{orderId}/process")
    public ResponseEntity<OrderResult> processOrder(
            @PathVariable String orderId) {
        Order order = orderService.getOrder(orderId);
        OrderResult result = orderService
            .processOrder(order);

        return result.isSuccess() ?
            ResponseEntity.ok(result) :
            ResponseEntity.badRequest().body(result);
    }

    @PostMapping("/{orderId}/ship")
    public ResponseEntity<OrderResult> shipOrder(
            @PathVariable String orderId) {
        Order order = orderService.getOrder(orderId);
        OrderResult result = orderService
            .shipOrder(order);

        return result.isSuccess() ?
            ResponseEntity.ok(result) :
            ResponseEntity.badRequest().body(result);
    }
}

5. Testing

5.1 Unit Testing

@ExtendWith(MockitoExtension.class)
class OrderCommandTest {
    @Mock
    private OrderRepository repository;

    @Mock
    private ShippingService shippingService;

    private Order order;
    private CommandInvoker invoker;

    @BeforeEach
    void setUp() {
        order = new Order("123");
        invoker = new CommandInvoker();
    }

    @Test
    void whenProcessOrder_thenStatusChanged() {
        // Given
        OrderCommand command = new ProcessOrderCommand(
            order, repository);

        // When
        invoker.executeCommand(command);

        // Then
        assertEquals(OrderStatus.PROCESSING, 
            order.getStatus());
        verify(repository).save(order);
    }

    @Test
    void whenUndoProcessOrder_thenStatusReverted() {
        // Given
        OrderCommand command = new ProcessOrderCommand(
            order, repository);
        invoker.executeCommand(command);

        // When
        invoker.undo();

        // Then
        assertEquals(OrderStatus.CANCELLED, 
            order.getStatus());
        verify(repository, times(2)).save(order);
    }
}

5.2 Integration Testing

@SpringBootTest
class OrderServiceIntegrationTest {
    @Autowired
    private OrderService orderService;

    @Autowired
    private OrderRepository repository;

    @Test
    void whenProcessAndShipOrder_thenSuccess() {
        // Given
        Order order = new Order("123");
        repository.save(order);

        // When
        OrderResult processResult = orderService
            .processOrder(order);
        OrderResult shipResult = orderService
            .shipOrder(order);

        // Then
        assertTrue(processResult.isSuccess());
        assertTrue(shipResult.isSuccess());
        assertEquals(OrderStatus.SHIPPED, 
            order.getStatus());
    }

    @Test
    void whenUndoShipOrder_thenRevertToProcessing() {
        // Given
        Order order = new Order("123");
        orderService.processOrder(order);
        orderService.shipOrder(order);

        // When
        orderService.undoLastOperation();

        // Then
        assertEquals(OrderStatus.PROCESSING, 
            order.getStatus());
    }
}

6. Lợi Ích và Nhược Điểm

6.1 Lợi Ích

  1. Tách biệt người gửi và người nhận
  2. Dễ dàng thêm commands mới
  3. Hỗ trợ undo/redo operations
  4. Hỗ trợ transaction-like functionality
  5. Đơn giản hóa các thao tác phức tạp

6.2 Nhược Điểm

  1. Tăng số lượng classes
  2. Có thể phức tạp hóa code
  3. Memory overhead với nhiều commands
  4. Khó xử lý dependencies giữa các commands

7. Best Practices

7.1 Command Factory

public class CommandFactory {
    private final Map<String, Supplier<Command>> 
        commandMap = new HashMap<>();

    public CommandFactory() {
        commandMap.put("process", 
            () -> new ProcessOrderCommand());
        commandMap.put("ship", 
            () -> new ShipOrderCommand());
        commandMap.put("cancel", 
            () -> new CancelOrderCommand());
    }

    public Command createCommand(
            String type, Object... args) {
        Supplier<Command> supplier = commandMap
            .get(type);
        if (supplier == null) {
            throw new IllegalArgumentException(
                "Unknown command type: " + type);
        }

        Command command = supplier.get();
        initializeCommand(command, args);
        return command;
    }

    private void initializeCommand(
            Command command, Object... args) {
        // Initialize command with arguments
    }
}

7.2 Composite Command

public class CompositeCommand implements Command {
    private List<Command> commands = new ArrayList<>();

    public void addCommand(Command command) {
        commands.add(command);
    }

    @Override
    public void execute() {
        for (Command command : commands) {
            command.execute();
        }
    }

    @Override
    public void undo() {
        for (int i = commands.size() - 1; i >= 0; i--) {
            commands.get(i).undo();
        }
    }
}

8. Common Issues và Solutions

8.1 Transaction Management

public class TransactionalCommand implements Command {
    private final Command command;
    private final TransactionManager txManager;

    @Override
    public void execute() {
        TransactionStatus status = 
            txManager.beginTransaction();
        try {
            command.execute();
            txManager.commit(status);
        } catch (Exception e) {
            txManager.rollback(status);
            throw e;
        }
    }

    @Override
    public void undo() {
        TransactionStatus status = 
            txManager.beginTransaction();
        try {
            command.undo();
            txManager.commit(status);
        } catch (Exception e) {
            txManager.rollback(status);
            throw e;
        }
    }
}

8.2 Asynchronous Execution

public class AsyncCommand implements Command {
    private final Command command;
    private final ExecutorService executor;
    private Future<?> future;

    @Override
    public void execute() {
        future = executor.submit(() -> {
            try {
                command.execute();
            } catch (Exception e) {
                handleExecutionError(e);
            }
        });
    }

    @Override
    public void undo() {
        if (future != null && !future.isDone()) {
            future.cancel(true);
        }

        executor.submit(() -> {
            try {
                command.undo();
            } catch (Exception e) {
                handleUndoError(e);
            }
        });
    }

    private void handleExecutionError(Exception e) {
        // Error handling logic
    }

    private void handleUndoError(Exception e) {
        // Error handling logic
    }
}

9. References

9.1 Books

  • Design Patterns: Elements of Reusable Object-Oriented Software
  • Head First Design Patterns
  • Clean Architecture by Robert C. Martin
  • Patterns of Enterprise Application Architecture

9.2 Online Resources