Menu
CoddyTech

Mẫu Pimpl

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 78 trên 104.

Thành ngữ Pimpl (con trỏ đến phần triển khai) là một kỹ thuật ẩn chi tiết triển khai của một lớp bằng cách chuyển chúng vào một lớp riêng được khai báo trước. Cách này giảm các phần phụ thuộc khi biên dịch và giữ cho các thành viên riêng tư thực sự ẩn khỏi tệp tiêu đề.

Ý tưởng cốt lõi rất đơn giản: thay vì khai báo các thành viên riêng tư trực tiếp trong lớp, bạn khai báo một con trỏ đến lớp triển khai chỉ được định nghĩa trong tệp mã nguồn:

// Widget.h
#include <memory>

class Widget {
public:
    Widget();
    ~Widget();
    void doSomething();
    
private:
    class Impl;  // Khai báo trước
    std::unique_ptr<Impl> pImpl;
};
// Widget.cpp
#include "Widget.h"
#include <iostream>

class Widget::Impl {
public:
    int data = 42;
    void process() { std::cout << "Processing: " << data << "\n"; }
};

Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default;

void Widget::doSomething() { pImpl->process(); }

Các lợi ích chính là tường lửa biên dịch và khả năng tương thích nhị phân. Khi bạn thay đổi lớp Impl, chỉ cần biên dịch lại tệp mã nguồn, không cần biên dịch lại mọi tệp có chứa tệp tiêu đề. Điều này giúp tăng tốc đáng kể thời gian xây dựng trong các dự án lớn.

Lưu ý rằng bạn phải định nghĩa hàm hủy (destructor) trong tệp nguồn, kể cả khi dùng định nghĩa mặc định, vì unique_ptr cần biết đầy đủ kiểu của Impl để xóa đối tượng đó. Đây là một lỗi thường gặp khi mới dùng Pimpl.

challenge icon

Thử thách

Dễ

Hãy xây dựng một bộ xử lý tin nhắn bảo mật bằng cách dùng thành ngữ Pimpl để ẩn chi tiết triển khai mã hóa khỏi tệp header. Ví dụ này cho thấy Pimpl tạo ra một lớp ngăn cách khi biên dịch: bất kỳ ai đưa header của bạn vào cũng sẽ không thấy cách xử lý tin nhắn bên trong.

Bạn sẽ tổ chức code trong ba tệp:

  • SecureMessage.h: Định nghĩa giao diện công khai cho lớp SecureMessage của bạn.

    Lớp của bạn cần có lớp Impl được khai báo trước và một std::unique_ptr trỏ đến lớp đó. Giao diện công khai cần có:

    • Một hàm khởi tạo nhận const std::string& làm tin nhắn gốc
    • Một hàm hủy (phải được khai báo ở đây và định nghĩa trong tệp .cpp)
    • Một phương thức setKey(int key) để đặt khóa mã hóa
    • Một phương thức getEncrypted() trả về tin nhắn đã mã hóa dưới dạng std::string
    • Một phương thức getOriginal() trả về tin nhắn gốc

    Header chỉ nên hiển thị giao diện công khai, không để lộ chi tiết triển khai về cách thức mã hóa.

  • SecureMessage.cpp: Định nghĩa lớp lồng nhau Impl và triển khai tất cả các phương thức.

    Lớp Impl cần lưu trữ tin nhắn gốc, khóa mã hóa (mặc định là 0) và xử lý logic mã hóa thực tế. Hãy dùng mật mã Caesar đơn giản để mã hóa: dịch chuyển từng ký tự theo giá trị khóa. Ví dụ, với khóa 3, 'a' trở thành 'd', còn 'z' quay vòng thành 'c'.

    Quá trình mã hóa chỉ nên tác động đến các chữ cái thường (a-z), giữ nguyên mọi ký tự khác. Hãy nhớ định nghĩa hàm hủy ở đây (ngay cả khi dùng định nghĩa mặc định), vì unique_ptr cần kiểu Impl hoàn chỉnh.

  • main.cpp: Đọc hai đầu vào:
    1. Một chuỗi tin nhắn (có thể chứa dấu cách)
    2. Một khóa mã hóa (số nguyên)

    Tạo một đối tượng SecureMessage, đặt khóa và hiển thị kết quả:

    1. In ra Original: theo sau là tin nhắn gốc
    2. In ra Encrypted: theo sau là tin nhắn đã mã hóa

Ví dụ, với đầu vào hello world và 3:

Original: hello world
Encrypted: khoor zruog

Với đầu vào xyz abc và 5:

Original: xyz abc
Encrypted: cde fgh

Hãy lưu ý rằng tệp header không tiết lộ điều gì về cách triển khai mật mã Caesar: đó chính là sức mạnh của Pimpl. Nếu sau này bạn đổi sang thuật toán mã hóa khác, chỉ cần biên dịch lại SecureMessage.cpp, không cần biên dịch lại bất kỳ tệp nào đưa header vào.

Tự làm thử

#include <iostream>
#include <string>
#include "SecureMessage.h"

int main() {
    // Đọc thông điệp (có thể chứa dấu cách)
    std::string message;
    std::getline(std::cin, message);
    
    // Đọc khóa mã hóa
    int key;
    std::cin >> key;
    
    // TODO: Tạo đối tượng SecureMessage từ thông điệp
    
    // TODO: Đặt khóa mã hóa
    
    // TODO: In ra "Original: ", sau đó là thông điệp gốc
    
    // TODO: In ra "Encrypted: ", sau đó là thông điệp đã mã hóa
    
    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