Hướng dẫn Java Design Pattern – Command

Đôi khi chúng ta cần gửi các yêu cầu cho các đối tượng mà không biết bất cứ điều gì về hoạt động được yêu cầu hoặc người nhận yêu cầu. Chẳng hạn chúng có một ứng dụng văn bản, khi click lên button undo/ redo, save, … yêu cầu sẽ được chuyển đến hệ thống xử lý, chúng ta sẽ không thể biết được đối tượng nào sẽ nhận xử lý, cách nó thực hiện như thế nào. Command Pattern là một Pattern được thiết kế cho những ứng dụng như vậy.

1. Command Pattern là gì?

Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.

Command Pattern là một trong những Pattern thuộc nhóm hành vi (Behavior Pattern). Nó cho phép chuyển yêu cầu thành đối tượng độc lập, có thể được sử dụng để tham số hóa các đối tượng với các yêu cầu khác nhau như log, queue (undo/redo), transtraction.

Nói cho dễ hiểu, Command Pattern cho phép tất cả những Request gửi đến object được lưu trữ trong chính object đó dưới dạng một object Command. Khái niệm Command Object giống như một class trung gian được tạo ra để lưu trữ các câu lệnh và trạng thái của object tại một thời điểm nào đó.

Command dịch ra nghĩa là ra lệnh. Commander nghĩa là chỉ huy, người này không làm mà chỉ ra lệnh cho người khác làm. Như vậy, phải có người nhận lệnh và thi hành lệnh. Người ra lệnh cần cung cấp một class đóng gói những mệnh lệnh. Người nhận mệnh lệnh cần phân biệt những interface nào để thực hiện đúng mệnh lệnh.

Command Pattern còn được biết đến như là Action hoặc Transaction.

2. Cài đặt Command Pattern như thế nào?

Các thành phần tham gia trong Command Pattern:

  • Command : là một interface hoặc abstract class, chứa một phương thức trừu tượng thực thi (execute) một hành động (operation). Request sẽ được đóng gói dưới dạng Command.
  • ConcreteCommand : là các implementation của Command. Định nghĩa một sự gắn kết giữa một đối tượng Receiver và một hành động. Thực thi execute() bằng việc gọi operation đang hoãn trên Receiver. Mỗi một ConcreteCommand sẽ phục vụ cho một case request riêng.
  • Client : tiếp nhận request từ phía người dùng, đóng gói request thành ConcreteCommand thích hợp và thiết lập receiver của nó.
  • Invoker : tiếp nhận ConcreteCommand từ Client và gọi execute() của ConcreteCommand để thực thi request.
  • Receiver : đây là thành phần thực sự xử lý business logic cho case request. Trong phương execute() của ConcreteCommand chúng ta sẽ gọi method thích hợp trong Receiver.

Như vậy, Client và Invoker sẽ thực hiện việc tiếp nhận request. Còn việc thực thi request sẽ do CommandConcreteCommand và Receiver đảm nhận.

2.1. Ví dụ Command Pattern trong ứng dụng mở tài khoản ngân hàng

Một hệ thống ngân hàng cung cấp ứng dụng cho khách hàng (client) có thể mở (open) hoặc đóng (close) tài khoản trực tuyến. Hệ thống này được thiết kế theo dạng module, mỗi module sẽ thực hiện một nhiệm vụ riêng của nó, chẳng hạn mở tài khoản (OpenAccount), đóng tài khoản (CloseAccount). Do hệ thống không biết mỗi module sẽ làm gì, nên khi có yêu cầu client (chẳng hạn clickOpenAccount, clickCloseAccount), nó sẽ đóng gói yêu cầu này và gọi module xử lý.

Ứng dụng của chúng ta bao gồm các lớp xử lý sau:

  • Account : là một request class.
  • Command : là một interface của Command Pattern, cung cấp phương thức execute().
  • OpenAccountCloseAccount : là các ConcreteCommand, cài đặt các phương thức của Command, sẽ thực hiện các xử lý thực tế.
  • BankApp : là một class, hoạt động như Invoker, gọi execute() của ConcreteCommand để thực thi request.
  • Client : tiếp nhận request từ phía người dùng, đóng gói request thành ConcreteCommand thích hợp và gọi thực thi các Command.

Account.java

package com.maixuanviet.patterns.behavioral.command.bank;
 
public class Account {
    private String name;
 
    public Account(String name) {
        this.name = name;
    }
 
    public void open() {
        System.out.println("Account [" + name + "] Opened\n");
    }
 
    public void close() {
        System.out.println("Account [" + name + "] Closed\n");
    }
}

Command.java

package com.maixuanviet.patterns.behavioral.command.bank;
 
public interface Command {
     
    void execute();
}

OpenAccount.java

package com.maixuanviet.patterns.behavioral.command.bank;
 
public class OpenAccount implements Command {
 
    private Account account;
 
    public OpenAccount(Account account) {
        this.account = account;
    }
 
    @Override
    public void execute() {
        account.open();
    }
}

CloseAccount.java

package com.maixuanviet.patterns.behavioral.command.bank;
 
public class CloseAccount implements Command {
 
    private Account account;
 
    public CloseAccount(Account account) {
        this.account = account;
    }
 
    @Override
    public void execute() {
        account.close();
    }
}

BankApp.java

package com.maixuanviet.patterns.behavioral.command.bank;
 
public class BankApp {
 
    private Command openAccount;
    private Command closeAccount;
 
    public BankApp(Command openAccount, Command closeAccount) {
        this.openAccount = openAccount;
        this.closeAccount = closeAccount;
    }
     
    public void clickOpenAccount() {
        System.out.println("User click open an account");
        openAccount.execute();
    }
     
    public void clickCloseAccount() {
        System.out.println("User click close an account");
        closeAccount.execute();
    }
}

Client.java

package com.maixuanviet.patterns.behavioral.command.bank;
 
public class Client {
 
    public static void main(String[] args) {
        Account account = new Account("maixuanviet");
 
        Command open = new OpenAccount(account);
        Command close = new CloseAccount(account);
        BankApp bankApp = new BankApp(open, close);
 
        bankApp.clickOpenAccount();
        bankApp.clickCloseAccount();
    }
}

Output của chương trình:

User click open an account
Account [maixuanviet] Opened
 
User click close an account
Account [maixuanviet] Closed

2.2. Ví dụ Command Pattern trong ứng dụng quản lý văn bản

Ứng dụng văn bản cần một chức năng để thêm hoặc lưu trữ những hành động undo hay redo.

Lớp Document chỉ cung cấp phương thức ghi thêm một dòng văn bản mới hoặc xóa một dòng văn bản đã ghi trước đó.

Chúng ta sẽ xây dựng một interface Command để cung cấp hành động undo/ redo. Để sử dụng Command chúng ta cần một DocumentInvoker, lớp này sử dụng tính năng của Stack để lưu lại lịch sử những lần thêm mới và những lần xóa, tương ứng với undoCommands và redoCommands.

Stack (ngăn xếp) là một cấu trúc dữ liệu trừu tượng hoạt động theo nguyên lý “vào sau ra trước” (Last In First Out (LIFO).

Một số phương thức của Stack:

  • push() : thêm phần tử trên đỉnh của Stack.
  • pop() : xóa phần tử trên đỉnh của Stack và trả về phần tử bị xóa.
  • peek() : lấy phần tử trên đỉnh của Stack, nhưng không xóa nó khỏi Stack.
  • clear() : xóa tất cả các phần tử trong Stack.
  • isEmpty() : kiểm tra Stack có chứa phần tử nào không.

Document.java

package com.maixuanviet.patterns.behavioral.command.document;
 
import java.util.Stack;
 
public class Document {
    private Stack<String> lines = new Stack<>();
 
    public void write(String text) {
        lines.push(text);
    }
 
    public void eraseLast() {
        if (!lines.isEmpty()) {
            lines.pop();
        }
    }
 
    public void readDocument() {
        for (String line : lines) {
            System.out.println(line);
        }
    }
}

Command.java

package com.maixuanviet.patterns.behavioral.command.document;
 
public interface Command {
    void undo();
 
    void redo();
}

DocumentEditorCommand.java

package com.maixuanviet.patterns.behavioral.command.document;
 
public class DocumentEditorCommand implements Command {
 
    private Document document;
    private String text;
 
    public DocumentEditorCommand(Document document, String text) {
        this.document = document;
        this.text = text;
        this.document.write(text);
    }
 
    public void undo() {
        document.eraseLast();
    }
 
    public void redo() {
        document.write(text);
    }
}

DocumentInvoker.java

package com.maixuanviet.patterns.behavioral.command.document;
 
import java.util.Stack;
 
public class DocumentInvoker {
    private Stack<Command> undoCommands = new Stack<>();
    private Stack<Command> redoCommands = new Stack<>();
    private Document document = new Document();
 
    public void undo() {
        if (!undoCommands.isEmpty()) {
            Command cmd = undoCommands.pop();
            cmd.undo();
            redoCommands.push(cmd);
        } else {
            System.out.println("Nothing to undo");
        }
    }
 
    public void redo() {
        if (!redoCommands.isEmpty()) {
            Command cmd = redoCommands.pop();
            cmd.redo();
            undoCommands.push(cmd);
        } else {
            System.out.println("Nothing to redo");
        }
    }
 
    public void write(String text) {
        Command cmd = new DocumentEditorCommand(document, text);
        undoCommands.push(cmd);
        redoCommands.clear();
    }
 
    public void read() {
        document.readDocument();
    }
}

Client.java

package com.maixuanviet.patterns.behavioral.command.document;
 
public class Client {
 
    public static void main(String[] args) {
        DocumentInvoker instance = new DocumentInvoker();
        instance.write("The 1st text. ");
        instance.undo();
        instance.read(); // EMPTY
         
        instance.redo();
        instance.read(); // The 1st text.
 
        instance.write("The 2nd text. ");
        instance.write("The 3rd text. ");
        instance.read(); // The 1st text. The 2nd text. The 3rd text. 
        instance.undo(); // The 1st text. The 2nd text.
        instance.undo(); // The 1st text.
        instance.undo(); // EMPTY
        instance.undo(); // Nothing to undo
    }
}

Output của chương trình:

The 1st text. 
The 1st text. 
The 2nd text. 
The 3rd text. 
Nothing to undo

3. Lợi ích của Command Pattern là gì?

  • Dễ dàng thêm các Command mới trong hệ thống mà không cần thay đổi trong các lớp hiện có. Đảm bảo Open/Closed Principle.
  • Tách đối tượng gọi operation từ đối tượng thực sự thực hiện operation. Giảm kết nối giữa Invoker và Receiver.
  • Cho phép tham số hóa các yêu cầu khác nhau bằng một hành động để thực hiện.
  • Cho phép lưu các yêu cầu trong hàng đợi.
  • Đóng gói một yêu cầu trong một đối tượng. Dễ dàng chuyển dữ liệu dưới dạng đối tượng giữa các thành phần hệ thống.

4. Sử dụng Command Pattern khi nào?

  • Khi cần tham số hóa các đối tượng theo một hành động thực hiện.
  • Khi cần tạo và thực thi các yêu cầu vào các thời điểm khác nhau.
  • Khi cần hỗ trợ tính năng undo, log , callback hoặc transaction.