Command Pattern là một mẫu thiết kế thuộc nhóm Behavioral Patterns (mẫu hành vi), giúp tách biệt đối tượng phát ra lệnh (gọi là invoker) và đối tượng thực thi lệnh (receiver) bằng cách đóng gói yêu cầu vào một đối tượng dưới dạng command. Mỗi lệnh (command) đại diện cho một hành động hoặc yêu cầu cụ thể, bao gồm thông tin về đối tượng nào cần thực thi hành động đó.

Mục đích của Command Pattern

Command Pattern cho phép đóng gói các yêu cầu dưới dạng các đối tượng command, giúp dễ dàng thực hiện các hành động như undo (hoàn tác), redo (làm lại), logging (ghi log), hoặc queue (xếp hàng đợi các lệnh). Bằng cách tách biệt logic xử lý lệnh và logic gọi lệnh, pattern này tạo ra sự linh hoạt trong việc xử lý và tổ chức các lệnh.

Động lực sử dụng Command Pattern

Giả sử bạn đang thiết kế một ứng dụng đồ họa với nhiều thao tác khác nhau như vẽ hình, xóa hình, thay đổi màu sắc,… Các thao tác này có thể được thực hiện theo nhiều cách khác nhau và có thể cần chức năng undo/redo. Nếu không sử dụng Command Pattern, bạn sẽ phải xử lý từng thao tác này một cách trực tiếp, điều này có thể khiến mã nguồn trở nên phức tạp và khó bảo trì.

Command Pattern cho phép đóng gói từng thao tác vào các đối tượng riêng lẻ, giúp việc thêm chức năng undo/redo, tổ chức các lệnh theo hàng đợi, hoặc ghi lại các hành động trở nên dễ dàng hơn mà không làm phức tạp mã nguồn chính.

Cấu trúc của Command Pattern

Command Pattern bao gồm các thành phần chính:

  • Command: Interface hoặc abstract class định nghĩa phương thức thực thi lệnh (execute).
  • ConcreteCommand: Lớp cụ thể triển khai interface Command, chứa tham chiếu đến đối tượng nhận lệnh (receiver) và thông tin về hành động cần thực hiện.
  • Invoker: Đối tượng phát ra lệnh, thường gọi phương thức execute() của một đối tượng Command.
  • Receiver: Đối tượng thực hiện hành động thực sự khi lệnh được gọi.
  • Client: Đối tượng tạo ra và thiết lập các đối tượng Command, liên kết chúng với các receiverinvoker.

Cách hoạt động của Command Pattern

  1. Client tạo ra các đối tượng Command và liên kết chúng với các receiver tương ứng.
  2. Invoker được cấu hình với các Command và khi cần, nó gọi phương thức execute() để thực hiện hành động.
  3. Receiver thực thi logic nghiệp vụ thực sự khi nhận được yêu cầu từ Command.

Ví dụ về Command Pattern

1. Ví dụ về một hệ thống điều khiển từ xa

Giả sử chúng ta thiết kế một hệ thống điều khiển từ xa với nhiều nút để bật và tắt các thiết bị điện như đèn, quạt, v.v.

// Interface Command định nghĩa phương thức execute()
public interface Command {
    void execute();
}

// Lớp cụ thể triển khai Command cho việc bật đèn
public class LightOnCommand implements Command {
    private Light light;

    public LightOnCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.on();  // Gọi phương thức on() của đối tượng Light
    }
}

// Lớp cụ thể triển khai Command cho việc tắt đèn
public class LightOffCommand implements Command {
    private Light light;

    public LightOffCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.off();  // Gọi phương thức off() của đối tượng Light
    }
}

// Lớp Receiver là đối tượng thực hiện hành động thực sự
public class Light {
    public void on() {
        System.out.println("Light is ON");
    }

    public void off() {
        System.out.println("Light is OFF");
    }
}

// Lớp Invoker giữ các Command và gọi execute() để thực hiện lệnh
public class RemoteControl {
    private Command command;

    public void setCommand(Command command) {
        this.command = command;
    }

    public void pressButton() {
        command.execute();  // Gọi phương thức execute() của Command
    }
}

// Sử dụng Command Pattern
public class Main {
    public static void main(String[] args) {
        Light livingRoomLight = new Light(); // Receiver

        // Tạo các lệnh cụ thể
        Command lightOn = new LightOnCommand(livingRoomLight);
        Command lightOff = new LightOffCommand(livingRoomLight);

        RemoteControl remote = new RemoteControl(); // Invoker

        // Bật đèn
        remote.setCommand(lightOn);
        remote.pressButton();  // Output: Light is ON

        // Tắt đèn
        remote.setCommand(lightOff);
        remote.pressButton();  // Output: Light is OFF
    }
}

Phân tích ví dụ

  • Command là interface định nghĩa phương thức execute().
  • LightOnCommandLightOffCommand là các lớp cụ thể thực hiện hành vi bật và tắt đèn.
  • Light là lớp Receiver, chứa logic thực thi thực sự cho việc bật và tắt đèn.
  • RemoteControlInvoker, nó lưu giữ đối tượng Command và gọi phương thức execute() để thực hiện lệnh.
  • Trong hàm main(), chúng ta tạo ra các lệnh bật/tắt đèn và cấu hình chúng cho điều khiển từ xa. Khi nhấn nút trên điều khiển, lệnh sẽ được thực hiện.

2. Ví dụ về undo/redo

Command Pattern dễ dàng hỗ trợ tính năng undo/redo nhờ việc lưu lại trạng thái trước và sau khi thực hiện lệnh.

// Interface Command với thêm phương thức undo()
public interface Command {
    void execute();
    void undo();
}

// Lớp cụ thể triển khai Command cho việc bật đèn
public class LightOnCommand implements Command {
    private Light light;

    public LightOnCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.on();
    }

    @Override
    public void undo() {
        light.off();  // Đảo ngược hành động
    }
}

// Lớp cụ thể triển khai Command cho việc tắt đèn
public class LightOffCommand implements Command {
    private Light light;

    public LightOffCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.off();
    }

    @Override
    public void undo() {
        light.on();  // Đảo ngược hành động
    }
}

Với các phương thức undo() này, bạn có thể dễ dàng gọi lại hành động trước đó để hoàn tác thao tác đã thực hiện.

Lợi ích của Command Pattern

  1. Tách biệt giữa người gọi lệnh và người thực thi: Người phát lệnh (invoker) không cần biết chi tiết về cách lệnh được thực thi, giúp giảm sự phụ thuộc giữa các thành phần.
  2. Dễ dàng mở rộng: Bạn có thể dễ dàng thêm các lệnh mới mà không cần sửa đổi mã nguồn hiện có.
  3. Hỗ trợ undo/redo: Vì mỗi lệnh đều có thể được đóng gói dưới dạng đối tượng, bạn có thể lưu lại và thực hiện lại các lệnh khi cần.
  4. Tổ chức lệnh: Command Pattern giúp tổ chức và quản lý các lệnh, đặc biệt khi cần phải xếp hàng hoặc xử lý các lệnh theo thời gian thực.

Nhược điểm của Command Pattern

  1. Tăng số lượng lớp: Việc đóng gói mỗi lệnh thành một lớp riêng biệt có thể làm tăng số lượng lớp trong dự án.
  2. Phức tạp: Đối với những thao tác đơn giản, Command Pattern có thể làm mã nguồn phức tạp hơn so với cách tiếp cận trực tiếp.

Kết luận

Command Pattern là một mẫu thiết kế mạnh mẽ cho phép đóng gói các yêu cầu thành các đối tượng độc lập, dễ dàng quản lý và mở rộng. Nó giúp giảm sự phụ thuộc giữa các thành phần của hệ thống, hỗ trợ undo/redo và có khả năng lưu trữ, xếp hàng các lệnh. Điều này làm cho Command Pattern trở nên lý tưởng trong các hệ thống yêu cầu tính linh hoạt và khả năng mở rộng cao.