+2

[C++ OOP Thực Chiến] Bài 4: Đóng gói (Encapsulation) - Đặt "lính gác" bảo vệ dữ liệu của bạn

Chào anh em, lại là mình đây. Tiếp nối series OOP Thực chiến, hôm nay chúng ta sẽ giải quyết một khái niệm mà 80% các bạn mới học hay nhầm lẫn với Trừu tượng (Abstraction) ở bài trước. Đó chính là Đóng gói (Encapsulation).

Nếu Abstraction là việc bạn thiết kế cái nút bấm (Interface) sao cho dễ dùng và giấu đi mớ dây điện phức tạp bên trong, thì Encapsulation chính là việc bạn bọc cái mớ dây điện đó vào một cái hộp sắt, khóa trái lại, không cho ai thọc tay vào làm hỏng mạch.

1. Bản chất của Đóng gói là gì?

Dưới góc nhìn của một kỹ sư Backend, hệ thống của bạn luôn phải đối mặt với những luồng dữ liệu rác, những thao tác sai lệch (có thể do user, hoặc do chính đồng nghiệp của bạn gọi nhầm hàm).

Đóng gói (Encapsulation) là kỹ thuật gom nhóm dữ liệu (Thuộc tính) và các hàm xử lý dữ liệu đó (Phương thức) vào chung một Class, đồng thời thiết lập các Quyền truy cập (Access Modifiers: private, protected, public) để hạn chế quyền can thiệp từ bên ngoài.

Quy tắc vàng trong OOP: Mọi thuộc tính (Data) đều phải là private. Không có ngoại lệ!

2. "Căn bệnh" Getter/Setter vô tri

Nhiều bạn học OOP máy móc thường làm thế này: Khai báo biến private, sau đó dùng IDE auto-generate ra một đống hàm get và set y hệt như sau:

private: 
    double balance;

public:
    void setBalance(double b) { this->balance = b; }
    double getBalance() { return this->balance; }

Viết như vậy thì... thà để public xừ cho rồi! Đóng gói không chỉ là giấu đi, mà là kiểm soát. Việc bạn mở toang cánh cửa setBalance cho phép bất kỳ ai cũng có thể gán balance = -999999 sẽ phá nát tính toàn vẹn dữ liệu (Data Integrity) của hệ thống.

3. Code Demo: Thiết kế Ví điện tử chuẩn Encapsulation

Hãy xem cách một kĩ sư thiết kế class DigitalWallet (Ví điện tử) với tư duy Đóng gói thực thụ. Thay vì cung cấp hàm setBalance vô tri, chúng ta cung cấp các nghiệp vụ (Business Logic) cụ thể như deposit (nạp tiền) và withdraw (rút tiền) đi kèm với Validation.

#include <iostream>
#include <string>

using namespace std;

class DigitalWallet {
private:
    // Dữ liệu nhạy cảm ĐƯỢC GIẤU KÍN (Chỉ nội bộ Class mới được chạm vào)
    string accountId;
    double balance;

    // Hàm nội bộ dùng để log lịch sử, bên ngoài không cần và không được phép gọi
    void logTransaction(string type, double amount) {
        cout << "[LOG] " << accountId << " | " << type << ": $" << amount 
             << " | So du moi: $" << balance << "\n";
    }

public:
    // Constructor
    DigitalWallet(string id) {
        accountId = id;
        balance = 0.0; // Khởi tạo ví luôn có 0 đồng
    }

    // GETTER: Chỉ cho phép XEM số dư, hoàn toàn an toàn
    double getBalance() const {
        return balance;
    }

    // KHÔNG CÓ SETTER cho balance. Chúng ta dùng các hàm nghiệp vụ:

    // Nghiệp vụ nạp tiền: Phải có "lính gác" kiểm tra số tiền nạp
    void deposit(double amount) {
        if (amount <= 0) {
            cout << "[ERROR] So tien nap phai lon hon 0!\n";
            return; // Chặn đứng thao tác sai
        }
        balance += amount;
        logTransaction("NAP TIEN", amount);
    }

    // Nghiệp vụ rút tiền: Phải kiểm tra số dư hiện tại
    void withdraw(double amount) {
        if (amount <= 0) {
            cout << "[ERROR] So tien rut phai lon hon 0!\n";
            return;
        }
        if (amount > balance) {
            cout << "[ERROR] So du khong du de rut $" << amount << "!\n";
            return; // Chặn đứng việc làm ví bị âm tiền
        }
        balance -= amount;
        logTransaction("RUT TIEN", amount);
    }
};

int main() {
    DigitalWallet myWallet("USER_999");

    // myWallet.balance = 1000; // LỖI COMPILER NGAY: Vì balance là private!
    
    // Thao tác qua các giao diện hợp lệ
    myWallet.deposit(500);    // Hợp lệ -> Số dư 500
    myWallet.deposit(-50);    // Bị lính gác chặn lại!
    
    myWallet.withdraw(1000);  // Bị lính gác chặn vì không đủ tiền!
    myWallet.withdraw(200);   // Hợp lệ -> Số dư 300

    cout << "\n=> So du cuoi cung: $" << myWallet.getBalance() << "\n";

    return 0;
}

Nhận xét: Bằng cách sử dụng private cho balance và yêu cầu client phải đi qua cửa deposit hoặc withdraw, Object myWallet của chúng ta luôn duy trì được một trạng thái đúng đắn (không bao giờ bị âm tiền). Đây chính là sức mạnh tối thượng của Đóng gói!

Tạm kết & Gợi mở

Đến đây, bạn đã có trong tay công cụ để tạo ra những Object cực kỳ an toàn (Encapsulation) và dễ sử dụng (Abstraction).

Tuy nhiên, khi dự án phình to, bạn sẽ gặp một vấn đề khác. Giả sử hệ thống của bạn không chỉ có DigitalWallet (Ví thường), mà sếp yêu cầu thêm VipWallet (Ví VIP được hoàn tiền khi nạp), rồi CryptoWallet (Ví coin). Chẳng lẽ chúng ta lại phải đi copy-paste toàn bộ code của DigitalWallet sang 2 class kia rồi sửa lại một chút?

Làm vậy thì vi phạm nghiêm trọng nguyên tắc DRY (Don't Repeat Yourself - Đừng lặp lại chính mình) của dân Coder!

Phải có cách nào đó để các Class mới có thể "hưởng sái" những dòng code đã viết từ Class cũ. Hẹn gặp lại các bạn ở Bài 5: Kế thừa (Inheritance) là gì? - Đừng copy-paste code nữa, hãy dùng não! Nhớ Upvote để mình có động lực ra bài sớm nhất nhé!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí