Menu
CoddyTech

Mẫu lệnh

Thuộc phần Lập trình hướng đối tượng trong hành trình C++ của Coddy. Bài học 96 trên 104.

Mẫu Command đóng gói một yêu cầu thành một đối tượng, cho phép bạn cấu hình các đối tượng khách bằng những yêu cầu khác nhau, xếp hàng các thao tác hoặc hỗ trợ chức năng hoàn tác. Mẫu này tách đối tượng gọi một thao tác khỏi đối tượng biết cách thực hiện thao tác đó.

Mẫu này có bốn thành phần chính: giao diện Command có phương thức execute(), Concrete Commands triển khai các hành động cụ thể, một Receiver thực hiện công việc thực tế và một Invoker kích hoạt các lệnh:

#include <iostream>
#include <memory>
#include <vector>

// Bộ nhận: biết cách thực hiện các thao tác
class Light {
public:
    void turnOn() { std::cout << "Light is ON\n"; }
    void turnOff() { std::cout << "Light is OFF\n"; }
};

// Giao diện lệnh
class Command {
public:
    virtual void execute() = 0;
    virtual void undo() = 0;
    virtual ~Command() = default;
};

// Các lệnh cụ thể
class LightOnCommand : public Command {
    Light& light;
public:
    LightOnCommand(Light& l) : light(l) {}
    void execute() override { light.turnOn(); }
    void undo() override { light.turnOff(); }
};

class LightOffCommand : public Command {
    Light& light;
public:
    LightOffCommand(Light& l) : light(l) {}
    void execute() override { light.turnOff(); }
    void undo() override { light.turnOn(); }
};

// Bộ gọi lệnh
class RemoteControl {
    std::vector<std::unique_ptr<Command>> history;
public:
    void pressButton(std::unique_ptr<Command> cmd) {
        cmd->execute();
        history.push_back(std::move(cmd));
    }
    
    void pressUndo() {
        if (!history.empty()) {
            history.back()->undo();
            history.pop_back();
        }
    }
};

Mỗi lệnh lưu tham chiếu đến đối tượng nhận và gọi phương thức phù hợp khi chạy. Đối tượng gọi không biết hành động nào sẽ xảy ra, nó chỉ gọi execute(). Khi lưu các lệnh vào lịch sử, bạn có thể dễ dàng triển khai thao tác hoàn tác bằng cách gọi phương thức undo() của từng lệnh.

Dùng Command khi bạn cần xếp hàng các thao tác, triển khai hoàn tác/làm lại hoặc tách bên gửi yêu cầu khỏi bên xử lý yêu cầu.

challenge icon

Thử thách

Dễ

Hãy xây dựng một trình soạn thảo văn bản có chức năng hoàn tác bằng mẫu Command. Bạn sẽ tạo một hệ thống gói các thao tác chỉnh sửa, chẳng hạn như chèn và xóa văn bản, thành các đối tượng lệnh. Nhờ đó, người dùng có thể thực hiện thao tác rồi hoàn tác theo thứ tự ngược lại, giống như trong một trình soạn thảo văn bản thực thụ.

Bạn sẽ sắp xếp code trong bốn tệp:

  • TextDocument.h: Tạo lớp nhận, lưu trữ và thao tác với nội dung văn bản thực tế.

    Lớp TextDocument cần lưu nội dung tài liệu dưới dạng chuỗi (string). Hãy cài đặt các phương thức sau:

    • insertText(const std::string& text): nối văn bản vào cuối nội dung
    • deleteText(int count): xóa count ký tự cuối cùng khỏi nội dung, không làm gì nếu count vượt quá độ dài nội dung
    • getContent(): trả về nội dung hiện tại dưới dạng tham chiếu const
  • Command.h: Định nghĩa giao diện lệnh và các lớp lệnh cụ thể.

    Tạo lớp trừu tượng Command với các phương thức thuần ảo execute() và undo(), cùng một hàm hủy ảo.

    Sau đó, hãy cài đặt hai lệnh cụ thể:

    • InsertCommand: nhận tham chiếu đến một TextDocument và một chuỗi cần chèn. Khi thực thi, lệnh sẽ chèn văn bản. Khi hoàn tác, lệnh sẽ xóa đúng số ký tự đã chèn.
    • DeleteCommand: nhận tham chiếu đến một TextDocument và số ký tự cần xóa. Lệnh cần lưu văn bản đã xóa để có thể khôi phục khi hoàn tác. Khi thực thi, lệnh sẽ xóa các ký tự ở cuối. Khi hoàn tác, lệnh sẽ chèn lại văn bản đã lưu.
  • TextEditor.h: Tạo lớp gọi lệnh, quản lý việc thực thi lệnh và lịch sử lệnh.

    Lớp TextEditor cần giữ tham chiếu đến một TextDocument và lưu lịch sử các lệnh đã thực thi bằng một vector gồm các unique_ptr<Command>. Hãy cài đặt:

    • executeCommand(std::unique_ptr<Command> cmd): thực thi lệnh và thêm lệnh đó vào lịch sử
    • undo(): hoàn tác lệnh gần nhất và xóa lệnh đó khỏi lịch sử, không làm gì nếu lịch sử trống
    • showContent(): in nội dung tài liệu hiện tại, hoặc [empty] nếu nội dung trống
  • main.cpp: Minh họa mẫu Command với chức năng hoàn tác.

    Đọc ba đầu vào:

    1. Văn bản thứ nhất cần chèn (chuỗi)
    2. Văn bản thứ hai cần chèn (chuỗi)
    3. Số ký tự cần xóa (số nguyên)

    Tạo một TextDocument và một TextEditor. Sau đó, thực hiện lần lượt các thao tác sau và hiển thị nội dung sau mỗi bước:

    1. Chèn văn bản thứ nhất, rồi hiển thị nội dung
    2. Chèn văn bản thứ hai, rồi hiển thị nội dung
    3. Xóa số ký tự đã chỉ định, rồi hiển thị nội dung
    4. Hoàn tác một lần, rồi hiển thị nội dung
    5. Hoàn tác thêm một lần nữa, rồi hiển thị nội dung

Ví dụ, với đầu vào Hello, World và 3:

Hello
Hello World
Hello Wo
Hello World
Hello

Với đầu vào Code, Editor và 6:

Code
CodeEditor
Code
CodeEditor
Code

Hãy chú ý cách mỗi lệnh biết cách tự đảo ngược thao tác của mình. DeleteCommand ghi nhớ nội dung đã xóa để có thể khôi phục, còn InsertCommand biết chính xác cần xóa bao nhiêu ký tự khi hoàn tác. Đây chính là sức mạnh của mẫu Command: các thao tác trở thành những đối tượng độc lập có thể được lưu trữ, thực thi và đảo ngược.

Tự làm thử

#include <iostream>
#include <string>
#include <memory>
#include "TextDocument.h"
#include "Command.h"
#include "TextEditor.h"

int main() {
    // Đọc đầu vào
    std::string firstText;
    std::string secondText;
    int deleteCount;
    
    std::getline(std::cin, firstText);
    std::getline(std::cin, secondText);
    std::cin >> deleteCount;

    // TODO: Tạo một TextDocument và một TextEditor
    
    // TODO: Thực hiện các thao tác theo thứ tự, hiển thị nội dung sau mỗi thao tác:
    // 1. Chèn đoạn văn bản đầu tiên, rồi hiển thị nội dung
    // 2. Chèn đoạn văn bản thứ hai, rồi hiển thị nội dung
    // 3. Xóa số ký tự đã chỉ định, rồi hiển thị nội dung
    // 4. Hoàn tác một lần, rồi hiển thị nội dung
    // 5. Hoàn tác thêm một lần nữa, rồi hiển thị nội dung
    
    // Gợi ý: Dùng std::make_unique<InsertCommand>(...) và 
    //       std::make_unique<DeleteCommand>(...) để tạo các lệnh

    return 0;
}
quiz iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Lập trình hướng đối tượng

Tự luyện tập: Trình biên dịch C++ online