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
- Tách biệt người gửi và người nhận
- Dễ dàng thêm commands mới
- Hỗ trợ undo/redo operations
- Hỗ trợ transaction-like functionality
- Đơn giản hóa các thao tác phức tạp
6.2 Nhược Điểm
- Tăng số lượng classes
- Có thể phức tạp hóa code
- Memory overhead với nhiều commands
- 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