Command Pattern là một trong các Mẫu thiết kế Hành vi (Behavioral Design Pattern). Nó được sử dụng để triển khai tính loose coupling (kết nối lỏng lẻo) trong mô hình yêu cầu-phản hồi (request-response).

Command Pattern là gì?
Trong mẫu thiết kế Command, request được gửi đến Invoker , sau đó Invoker chuyển yêu cầu cho đối tượng Command đã được đóng gói. Đối tượng Command sẽ chuyển tiếp yêu cầu đến phương thức phù hợp của Receiverđể thực hiện hành động cụ thể.
Client sẽ tạo đối tượng Receiver, sau đó gắn nó vào Command. Tiếp theo, nó tạo đối tượng Invoker và gắn Command vào để có thể gọi một hành động. Khi client thực thi hành động, yêu cầu sẽ được xử lý thông qua Command và Receiver tương ứng.
Ví dụ về Command Pattern
Chúng ta sẽ xem xét một tình huống thực tế nơi có thể áp dụng Command Pattern. Giả sử chúng ta muốn xây dựng một tiện ích File System với các phương thức mở, ghi và đóng tập tin. Tiện ích này cần hỗ trợ nhiều hệ điều hành khác nhau như Windows và Unix.
Để triển khai, trước hết chúng ta cần tạo các lớp Receiver, là những lớp sẽ đảm nhiệm vai trò thực hiện toàn bộ công việc. Vì trong Java, chúng ta lập trình dựa trên interface nên có thể định nghĩa một giao diện FileSystemReceiver và xây dựng các lớp triển khai riêng biệt cho từng hệ điều hành như Windows, Unix, Solaris…
Các lớp Receiver trong Command Pattern
package com.journaldev.design.command;
public interface FileSystemReceiver {
void openFile();
void writeFile();
void closeFile();
}
Interface FileSystemReceiver định nghĩa hợp đồng cho các lớp triển khai. Để đơn giản, tôi sẽ tạo hai phiên bản của lớp Receiver dùng cho hệ thống Unix và Windows.
package com.journaldev.design.command;
public class UnixFileSystemReceiver implements FileSystemReceiver {
@Override
public void openFile() {
System.out.println("Đang mở file trên hệ điều hành Unix");
}
@Override
public void writeFile() {
System.out.println("Đang ghi file trên hệ điều hành Unix");
}
@Override
public void closeFile() {
System.out.println("Đang đóng file trên hệ điều hành Unix");
}
}
package com.journaldev.design.command;
public class WindowsFileSystemReceiver implements FileSystemReceiver {
@Override
public void openFile() {
System.out.println("Đang mở file trên hệ điều hành Windows");
}
@Override
public void writeFile() {
System.out.println("Đang ghi file trên hệ điều hành Windows");
}
@Override
public void closeFile() {
System.out.println("Đang đóng file trên hệ điều hành Windows");
}
}
Bạn có để ý đến annotation @Override không? Nếu thắc mắc lý do sử dụng annotation này, bạn nên đọc thêm về annotation trong Java và những lợi ích của @Override. Giờ thì khi các lớp Receiver đã sẵn sàng, chúng ta có thể chuyển sang triển khai các lớp Command.
Giao diện và các triển khai của Command Pattern
Chúng ta có thể sử dụng interface hoặc abstract class để tạo lớp cơ sở cho Command, quyết định này sẽ phụ thuộc vào yêu cầu cụ thể của bạn. Trong trường hợp này, chúng ta chọn dùng interface vì không có sẵn phần cài đặt mặc định.
package com.journaldev.design.command;
public interface Command {
void execute();
}
Bây giờ, chúng ta cần tạo các lớp triển khai cho tất cả các hành động khác nhau mà Receiver thực hiện. Vì có ba hành động, chúng ta sẽ tạo ba lớp triển khai Command. Mỗi lớp sẽ chuyển tiếp yêu cầu đến phương thức phù hợp của Receiver.
package com.journaldev.design.command;
public class OpenFileCommand implements Command {
private FileSystemReceiver fileSystem;
public OpenFileCommand(FileSystemReceiver fs) {
this.fileSystem = fs;
}
@Override
public void execute() {
// Lệnh open chuyển tiếp yêu cầu đến phương thức openFile
this.fileSystem.openFile();
}
}
package com.journaldev.design.command;
public class CloseFileCommand implements Command {
private FileSystemReceiver fileSystem;
public CloseFileCommand(FileSystemReceiver fs) {
this.fileSystem = fs;
}
@Override
public void execute() {
this.fileSystem.closeFile();
}
}
package com.journaldev.design.command;
public class WriteFileCommand implements Command {
private FileSystemReceiver fileSystem;
public WriteFileCommand(FileSystemReceiver fs) {
this.fileSystem = fs;
}
@Override
public void execute() {
this.fileSystem.writeFile();
}
}
Khi đã có Receiver và Command, chúng ta có thể triển khai lớp Invoker.
Lớp Invoker trong mẫu thiết kếCommand
Invoker là một lớp đơn giản đóng gói Command và chuyển yêu cầu đến đối tượng Command để xử lý.
package com.journaldev.design.command;
public class FileInvoker {
public Command command;
public FileInvoker(Command c) {
this.command = c;
}
public void execute() {
this.command.execute();
}
}
Phần triển khai tiện ích File System đã hoàn tất và bây giờ chúng ta có thể bắt đầu viết chương trình client đơn giản sử dụng mẫu thiết kế Command. Tuy nhiên, trước đó tôi sẽ cung cấp một phương thức tiện ích để tạo đối tượng FileSystemReceiver phù hợp. Vì có thể dùng lớp System để lấy thông tin hệ điều hành nên chúng ta sẽ áp dụng cách này hoặc cũng có thể sử dụng Factory Pattern để trả về kiểu phù hợp tùy theo tham số đầu vào.
package com.journaldev.design.command;
public class FileSystemReceiverUtil {
public static FileSystemReceiver getUnderlyingFileSystem() {
String osName = System.getProperty("os.name");
System.out.println("Hệ điều hành đang sử dụng: " + osName);
if (osName.contains("Windows")) {
return new WindowsFileSystemReceiver();
} else {
return new UnixFileSystemReceiver();
}
}
}
Bây giờ chúng ta sẽ bắt đầu xây dựng chương trình client ví dụ theo Command Design Pattern để sử dụng tiện ích hệ thống tập tin.
package com.journaldev.design.command;
public class FileSystemClient {
public static void main(String[] args) {
// Tạo đối tượng receiver
FileSystemReceiver fs = FileSystemReceiverUtil.getUnderlyingFileSystem();
// Tạo command và liên kết với receiver
OpenFileCommand openFileCommand = new OpenFileCommand(fs);
// Tạo invoker và liên kết với Command
FileInvoker file = new FileInvoker(openFileCommand);
// Thực hiện hành động trên đối tượng invoker
file.execute();
WriteFileCommand writeFileCommand = new WriteFileCommand(fs);
file = new FileInvoker(writeFileCommand);
file.execute();
CloseFileCommand closeFileCommand = new CloseFileCommand(fs);
file = new FileInvoker(closeFileCommand);
file.execute();
}
}
Lưu ý, chương trình client có trách nhiệm tạo đối tượng command với loại phù hợp. Chẳng hạn, nếu muốn ghi tập tin thì bạn không nên tạo đối tượng CloseFileCommand. Client cũng chịu trách nhiệm gắn receiver vào command, sau đó gắn Command vào lớp Invoker. Kết quả đầu ra của chương trình ví dụ mẫu Command ở trên là:
Hệ điều hành đang sử dụng: Mac OS X
Đang mở file trên hệ điều hành Unix
Đang ghi file trên hệ điều hành Unix
Đang đóng file trên hệ điều hành Unix
Sơ đồ lớp của Command Pattern
Đây là sơ đồ lớp cho việc triển khai tiện ích hệ thống tệp của chúng ta. (Nơi để hình ảnh sơ đồ lớp)

Những điểm quan trọng của Command Pattern
- Command là thành phần cốt lõi của Command Pattern, định nghĩa contract cho việc triển khai.
- Phần triển khaiReceiver được tách riêng khỏi phần triển khai Command.
- Mỗi lớp triển khai Command sẽ chọn phương thức cụ thể cần gọi trên đối tượng Receiver, và với mỗi phương thức trong Receiver sẽ tương ứng với một lớp Command riêng. Lớp command hoạt động như một cầu nối giữa Receiver và các phương thức hành động.
- Lớp Invoker chỉ đóng vai trò chuyển tiếp yêu cầu từ client đến đối tượng Command.
- Client chịu trách nhiệm khởi tạo các đối tượng Command và Receiver phù hợp, sau đó liên kết chúng lại với nhau.
- Client cũng chịu trách nhiệm khởi tạo đối tượng Invoker, gán đối tượng Command vào đó và thực thi phương thức hành động.
- Command Pattern rất dễ mở rộng. Chúng ta có thể thêm các phương thức hành động mới trong Receiver và tạo các lớp triển khai Command tương ứng mà không cần thay đổi mã nguồn phía client.
- Tuy nhiên, nhược điểm của Command Pattern là mã nguồn sẽ trở nên cồng kềnh và khó theo dõi nếu có quá nhiều phương thức hành động và quá nhiều mối liên kết.
Ví dụ về Command Pattern trong JDK
Runnable (java.lang.Runnable) và Swing Action (javax.swing.Action) là những ví dụ sử dụng Command Pattern.