Tính Đóng Gói (Encapsulation)

"Trong lập trình hướng đối tượng, tính đóng gói (Encapsulation) là một nguyên lý cốt lõi giúp bảo vệ dữ liệu và kiểm soát quyền truy cập vào các thành phần của đối tượng. Điều này không chỉ nâng cao tính bảo mật mà còn làm cho mã nguồn trở nên dễ bảo trì hơn. Dưới đây là những điều cơ bản bạn cần biết về tính đóng gói trong Java."

Ⅰ. Đóng gói (Encapsulation) là gì?

Đóng gói (Encapsulation) là một khái niệm quan trọng trong lập trình hướng đối tượng (OOP) của Java. Nó đề cập đến việc đóng gói dữ liệu (các biến) và các phương thức thao tác trên dữ liệu đó trong một lớp, nhằm che giấu thông tin bên trong lớp và chỉ cung cấp những gì cần thiết thông qua các phương thức công khai (public methods).

Viên nang thuốc là ví dụ kinh điển và được sử dụng nhiều nhất. Tính đóng gói giống như viên nang thuốc chứa nhiều thành phần thuốc khác nhau, tính đóng gói bao bọc dữ liệu (thuộc tính) và các phương thức thao tác trên dữ liệu (phương thức) vào một đơn vị duy nhất (lớp). Viên nang bảo vệ thuốc khỏi các tác nhân bên ngoài, tương tự như lớp bảo vệ dữ liệu khỏi truy cập và sửa đổi trái phép.

Tính đóng gói (Encapsulation) trong Java - sơ đồ minh họa
Ảnh mô tả tính đóng gói (Encapsulation) trong Java.

Các thuật ngữ liên quan đến đóng gói:

↳ Lớp (Class): Là một bản thiết kế cho các đối tượng, định nghĩa các thuộc tính và phương thức của chúng.

↳ Đối tượng (Object): Là một thực thể được tạo ra từ một lớp, nó có các thuộc tính và hành vi riêng.

↳ Thuộc tính (Attribute): Là một đặc tính của một đối tượng, thường được biểu diễn bằng các biến.

↳ Phương thức (Method): Là một hành động mà một đối tượng có thể thực hiện, thường được biểu diễn bằng các hàm.

↳ Kiểm soát truy cập (Access modifier): Sử dụng các từ khóa như public, private, protected để xác định mức độ truy cập vào các thành viên của một lớp.

↳ Getter và setter: Là các phương thức đặc biệt dùng để truy xuất và cập nhật giá trị của các thuộc tính private.

Lợi ích của đóng gói (Encapsulation):

↳ Kiểm soát dữ liệu: Bạn có thể kiểm soát việc truy cập và thay đổi dữ liệu. Ví dụ, trong setter của grade, bạn có thể kiểm tra xem điểm có hợp lệ (không âm) trước khi cập nhật giá trị.

↳ Ẩn dữ liệu (Data Hiding): Các lớp khác không thể truy cập trực tiếp vào dữ liệu, giúp bảo vệ dữ liệu khỏi những thay đổi không mong muốn.

↳ Tính linh hoạt: Bạn có thể dễ dàng thay đổi cách thức truy cập và thao tác dữ liệu bằng cách sửa đổi setter và getter.

↳ Dễ dàng kiểm thử: Các lớp được đóng gói dễ dàng kiểm thử riêng lẻ vì dữ liệu được bảo vệ.

Ⅱ. Cách thức hoạt động của tính đóng gói (Encapsulation)

Tính đóng gói (encapsulation) được thực hiện bằng cách khai báo các thuộc tính (biến) của lớp là private và cung cấp các phương thức công khai (public) để truy cập và sửa đổi các thuộc tính này. Các phương thức công khai này được gọi là các phương thức getter và setter. Dưới đây là cú pháp cơ bản để thực hiện tính đóng gói:

↳ Khai báo dữ liệu thành private: Bạn khai báo các biến lưu trữ dữ liệu (như accountNumber, balance, và accountHolderName) thành private. Điều này có nghĩa là các lớp khác không thể truy cập trực tiếp vào dữ liệu này.

↳ Sử dụng phương thức công khai setter và getter: Để cho phép các lớp khác truy cập và thay đổi dữ liệu được kiểm soát, bạn tạo các phương thức đặc biệt:

↳ Setter: Cho phép thiết lập giá trị cho biến. Ví dụ: set accountHolderName (String newaccountHolderName) để đặt tên mới cho tài khoản.

↳ Getter: Cho phép lấy giá trị của biến. Ví dụ: get accountHolderName() để lấy tên của chủ tài khoản.

Ⅲ. Ví dụ về tính đóng gói (Encapsulation)

Dưới đây là một ví dụ về tính đóng gói (encapsulation) trong một lớp Tài Khoản Ngân Hàng. Trong ví dụ này, các thuộc tính của tài khoản ngân hàng như số tài khoản, số dư, và tên chủ tài khoản được đóng gói bên trong lớp và chỉ có thể truy cập thông qua các phương thức công khai.

Ví dụ: BankAccount.java

public class BankAccount {
    // Các thuộc tính được đóng gói
    private String accountNumber;
    private double balance;
    private String accountHolderName;

    // Constructor để khởi tạo tài khoản ngân hàng
    public BankAccount(String accountNumber, String accountHolderName, double initialBalance) {
        this.accountNumber = accountNumber;
        this.accountHolderName = accountHolderName;
        this.balance = initialBalance;
    }

    // Phương thức để lấy số tài khoản
    public String getAccountNumber() {
        return accountNumber;
    }

    // Phương thức để lấy tên chủ tài khoản
    public String getAccountHolderName() {
        return accountHolderName;
    }

    // Phương thức để lấy số dư
    public double getBalance() {
        return balance;
    }

    // Phương thức để nạp tiền vào tài khoản
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            System.out.println("Nạp tiền thành công. Số dư mới: " + balance);
        } else {
            System.out.println("Số tiền nạp phải lớn hơn 0.");
        }
    }

    // Phương thức để rút tiền từ tài khoản
    public void withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            System.out.println("Rút tiền thành công. Số dư mới: " + balance);
        } else {
            System.out.println("Số tiền rút không hợp lệ hoặc không đủ số dư.");
        }
    }
}

Giải thích đoạn mã trên:

↳ Các thuộc tính được đóng gói: accountNumber, balance, và accountHolderName được khai báo là private, chỉ có thể truy cập bên trong lớp BankAccount.

↳ Constructor: Được sử dụng để khởi tạo các thuộc tính của tài khoản ngân hàng.

↳ Các phương thức công khai Getter và Setter:

↳ getAccountNumber() và setAccountNumber(String accountNumber) để lấy và gán giá trị cho thuộc tính accountNumber.

↳ getAccountHolderName() và setAccountHolderName(String accountHolderName) để lấy và gán giá trị cho thuộc tính accountHolderName.

↳ getBalance() và setBalance(double balance) để lấy và gán giá trị cho thuộc tính balance.

Lớp Main: sử dụng phương thức getter.

Tạo một đối tượng BankAccount và sử dụng các phương thức getter công khai để tương tác lấy thông tin của tài khoản ngân hàng.

Ví dụ: Main.java

public class Main {
    public static void main(String[] args) {
        // Tạo một đối tượng tài khoản ngân hàng
        BankAccount account = new BankAccount("123456789", "Dương", 1000.0);

        // In thông tin tài khoản
        System.out.println("Số tài khoản: " + account.getAccountNumber());
        System.out.println("Tên chủ tài khoản: " + account.getAccountHolderName());
        System.out.println("Số dư hiện tại: " + account.getBalance());

        // Nạp tiền vào tài khoản
        account.deposit(500.0);

        // Rút tiền từ tài khoản
        account.withdraw(200.0);

        // Thử rút số tiền lớn hơn số dư hiện tại
        account.withdraw(1500.0);
    }
}

Kết quả của chương trình là:

Số tài khoản: 123456789
Tên chủ tài khoản: Dương
Số dư hiện tại: 1000.0
Nạp tiền thành công. Số dư mới: 1500.0
Rút tiền thành công. Số dư mới: 1300.0
Số tiền rút không hợp lệ hoặc không đủ số dư.

Lớp Main: sử dụng phương thức setter.

Tạo một đối tượng BankAccount, sử dụng các phương thức setter để thay đổi tên chủ tài khoản và sử dụng phương thức getter để tương tác lấy thông tin của chủ tài khoản ngân hàng mới.

Ví dụ: Main.java

public class Main {
    public static void main(String[] args) {
        // Tạo một đối tượng tài khoản ngân hàng
        BankAccount account = new BankAccount("123456789", "Dương", 1000.0);
        
        // Thay đổi tên chủ tài khoản và in lại thông tin
        account.setAccountHolderName("Nguyễn Văn A");
        
        // In thông tin tài khoản
        System.out.println("Số tài khoản: " + account.getAccountNumber());
        System.out.println("Tên chủ tài khoản: " + account.getAccountHolderName());
        System.out.println("Số dư hiện tại: " + account.getBalance());
    }
}

Kết quả của chương trình là:

Số tài khoản: 123456789
Tên chủ tài khoản: Nguyễn Văn A
Số dư hiện tại: 1000.0

Trong ví dụ này minh họa cách tính đóng gói bảo vệ dữ liệu của tài khoản ngân hàng và chỉ cho phép truy cập và thay đổi dữ liệu thông qua các phương thức công khai, đảm bảo tính an toàn và toàn vẹn của dữ liệu.

Ⅳ. Ví dụ về vấn đề khi không có tính đóng gói (Encapsulation)

Dưới đây là một ví dụ minh họa cho thấy các vấn đề có thể xảy ra khi không sử dụng tính đóng gói trong lớp BankAccount:

Ví dụ: BankAccount.java

public class BankAccount {
    // Các thuộc tính không được đóng gói
    public String accountNumber;
    public double balance;
    public String accountHolderName;

    // Constructor để khởi tạo tài khoản ngân hàng
    public BankAccount(String accountNumber, String accountHolderName, double initialBalance) {
        this.accountNumber = accountNumber;
        this.accountHolderName = accountHolderName;
        this.balance = initialBalance;
    }

    // Các phương thức nạp tiền và rút tiền
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            System.out.println("Nạp tiền thành công. Số dư mới: " + balance);
        } else {
            System.out.println("Số tiền nạp phải lớn hơn 0.");
        }
    }

    public void withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            System.out.println("Rút tiền thành công. Số dư mới: " + balance);
        } else {
            System.out.println("Số tiền rút không hợp lệ hoặc không đủ số dư.");
        }
    }
}

Lớp Main

Ví dụ: Main.java

public class Main {
    public static void main(String[] args) {
        // Tạo một đối tượng tài khoản ngân hàng
        BankAccount account = new BankAccount("123456789", "Dương", 1000.0);

        // Truy cập và thay đổi trực tiếp các thuộc tính
        account.balance = -500.0; // Số dư không hợp lệ
        account.accountHolderName = ""; // Tên chủ tài khoản không hợp lệ

        // In thông tin tài khoản
        System.out.println("Số tài khoản: " + account.accountNumber);
        System.out.println("Tên chủ tài khoản: " + account.accountHolderName);
        System.out.println("Số dư hiện tại: " + account.balance);
    }
}

Kết quả của chương trình là:

Số tài khoản: 123456789
Tên chủ tài khoản:
Số dư hiện tại: -500.0

Vấn đề trong ví dụ trên:

↳ Số dư âm: Số dư tài khoản có thể bị đặt thành giá trị âm, điều này không hợp lệ trong thực tế.

↳ Tên chủ tài khoản trống: Tên chủ tài khoản có thể bị đặt thành giá trị trống, điều này cũng không hợp lệ.

↳ Thiếu tính kiểm soát: Không có cách nào để kiểm soát hoặc xác minh các giá trị được đặt cho các thuộc tính.

Nếu không có tính đóng gói (encapsulation), các thuộc tính của lớp BankAccount sẽ không được bảo vệ và có thể bị truy cập hoặc thay đổi trực tiếp từ bên ngoài. Điều này có thể dẫn đến nhiều vấn đề, bao gồm:

Bảo mật và an toàn dữ liệu:

↳ Truy cập trái phép: Các thuộc tính như accountNumber, balance và accountHolderName có thể bị truy cập và thay đổi từ bất kỳ đâu trong chương trình, dẫn đến việc dữ liệu nhạy cảm của tài khoản ngân hàng bị xâm phạm.

↳ Sửa đổi không hợp lệ: Các giá trị của thuộc tính có thể bị thay đổi thành các giá trị không hợp lệ hoặc không mong muốn mà không có bất kỳ kiểm tra hoặc xác minh nào. Ví dụ, số dư tài khoản (balance) có thể bị đặt thành số âm hoặc số lượng không hợp lệ.

Khó khăn trong bảo trì và Phát triển:

↳ Mã lộn xộn và phức tạp: Việc cho phép truy cập trực tiếp đến các thuộc tính từ nhiều phần khác nhau của chương trình sẽ làm cho mã nguồn trở nên khó đọc, khó hiểu và khó bảo trì.

↳ Thiếu tính kiểm soát: Không có khả năng kiểm soát khi nào và làm thế nào các thuộc tính được thay đổi, làm cho việc gỡ lỗi và bảo trì trở nên phức tạp hơn.

Thiếu khả năng kiểm tra và xác minh:

↳ Không thể kiểm tra giá trị: Không có cách nào để đảm bảo rằng các giá trị của thuộc tính luôn hợp lệ. Ví dụ, không có cách nào để kiểm tra xem số tiền nạp hoặc rút có hợp lệ hay không trước khi thực hiện thay đổi.

Câu Nói Truyền Cảm Hứng

“Không ai sinh ra đã giỏi. Mọi thành công đều bắt đầu từ một bước nhỏ.” – Lao Tzu

Không Gian Tích Cực

“Chúc bạn một ngày mới đầy năng lượng và sự sáng tạo, luôn tiến về phía trước.”