CodeGym /Các khóa học /JAVA 25 SELF /Nguyên tắc đóng gói (encapsulation), vì sao cần thiết

Nguyên tắc đóng gói (encapsulation), vì sao cần thiết

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. Định nghĩa encapsulation

Encapsulation là một trong những nguyên lý nền tảng của lập trình hướng đối tượng (OOP). Nói một cách đơn giản, encapsulation là khả năng giấu “ruột gan” của một đối tượng và chỉ cho phép truy cập thông qua những “cánh cửa” được thiết kế sẵn — các phương thức công khai.

Hãy tưởng tượng một máy pha cà phê hiện đại. Người dùng chỉ thấy các nút và màn hình — họ không cần biết bên trong nồi hơi, bơm và ống hoạt động ra sao. Họ bấm “Cappuccino” — và nhận kết quả. Mọi thứ bên trong đều được ẩn đi. Đó chính là encapsulation!

Trong Java (và các ngôn ngữ OOP khác), encapsulation đạt được nhờ:

  • Che giấu dữ liệu — các trường của lớp được khai báo là private (hoặc ít nhất không phải public).
  • Giao diện công khai — chỉ “đưa ra ngoài” những phương thức thực sự cần cho người dùng của đối tượng.

Sơ đồ: encapsulation trông như thế nào

+-------------------------------+
|         Class Student         |
|-------------------------------|
| - name: String                |  // private field
| - age: int                    |  // private field
|-------------------------------|
| + getName(): String           |  // public method
| + setName(String): void       |  // public method
| + getAge(): int               |  // public method
| + setAge(int): void           |  // public method
+-------------------------------+

Ở đây ký hiệu - nghĩa là private (bị ẩn), còn +public (có thể truy cập từ bên ngoài).

Getter và setter là gì?

Trước khi bàn vì sao cần encapsulation, hãy nhanh chóng làm quen với gettersetter — các phương thức đặc biệt giúp chúng ta “giao tiếp” với các trường private của lớp.

Getter — phương thức lấy giá trị của một trường private. Thông thường có tên là getFieldName().

Setter — phương thức thiết lập giá trị cho một trường private. Thông thường có tên là setFieldName(value).

Ví dụ đơn giản:

public class Student {
    private String name; // trường private — không thấy từ bên ngoài
    
    // Getter — "hãy cho tôi tên sinh viên"
    public String getName() {
        return name;
    }
    
    // Setter — "hãy đặt tên cho sinh viên"
    public void setName(String name) {
        this.name = name;
    }
}

Nó hoạt động như sau:

Student student = new Student();
student.setName("Vasya");           // thiết lập tên qua setter
String name = student.getName();    // lấy tên qua getter

Hãy nghĩ về getter và setter như các “lời đề nghị lịch sự” với đối tượng: thay vì thò tay vào túi nó (student.name = "Vasya"), ta lịch sự yêu cầu: “Vui lòng đặt tên” (student.setName("Vasya")).

Nói trước một chút: vài bài nữa chúng ta sẽ học kỹ về getter và setter, biết các bí quyết và cách dùng hiệu quả. Hiện tại chỉ cần nắm ý tưởng chính là đủ!

2. Vì sao cần encapsulation?

Bảo vệ dữ liệu khỏi việc sử dụng không đúng

Nếu tất cả các trường của lớp đều công khai (public), bất kỳ mã bên ngoài nào cũng có thể trực tiếp thay đổi giá trị của chúng theo bất kỳ cách nào:

Student s = new Student();
s.age = -1000; // Ôi, sinh viên ma cà rồng!

Điều này rất nguy hiểm! Chương trình của bạn có thể bắt đầu hành xử khó lường, và lỗi sẽ xuất hiện ở những nơi không ngờ tới.

Khả năng thay đổi hiện thực bên trong mà không ảnh hưởng đến mã bên ngoài

Encapsulation cho phép bạn thay đổi cấu trúc bên trong của lớp mà không làm hỏng mã đang sử dụng nó. Ví dụ, bạn có thể thay đổi cách lưu trữ dữ liệu hoặc thêm kiểm tra hợp lệ trong các phương thức, và người dùng lớp sẽ không nhận ra — họ vẫn gọi các phương thức như cũ.

Cải thiện khả năng đọc và bảo trì mã

Khi các chi tiết bên trong được ẩn đi, giao diện bên ngoài sẽ gọn gàng và dễ hiểu hơn. Lập trình viên sử dụng lớp của bạn không cần hiểu nó hoạt động thế nào bên trong — họ chỉ cần biết có những phương thức nào và chúng làm gì.

Ví dụ đời thường

Hãy nhớ cách bạn dùng điện thoại thông minh. Bạn không nghĩ về cách xử lý thao tác chạm, cấu tạo pin hay module kết nối hoạt động ra sao. Bạn chỉ gọi các chức năng cần thông qua giao diện dễ hiểu (biểu tượng, nút bấm). Nếu nhà sản xuất thay đổi hiện thực bên trong, bạn thậm chí còn không nhận ra.

Ví dụ thực tế trong mã

Giả sử có lớp BankAccount. Trong phiên bản cũ, số dư được lưu dưới dạng chuỗi có dấu chấm phân tách, ví dụ "1.000.50". Sau đó lập trình viên quyết định lưu số dư dưới dạng số double. Nếu trường là công khai, toàn bộ mã cũ truy cập trực tiếp account.balance sẽ hỏng.

Nhưng nếu ta dùng encapsulation và ẩn trường, chỉ cung cấp các phương thức deposit()getBalance(), mã bên ngoài thậm chí sẽ không biết có thay đổi:

public class BankAccount {
    private double balance; // trường được ẩn

    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }

    public double getBalance() {
        return balance;
    }
}

Bây giờ nếu ngày mai ta muốn lưu số dư, chẳng hạn, theo đơn vị cent (long), chỉ cần thay đổi hiện thực bên trong lớp; toàn bộ mã còn lại gọi deposit()getBalance() vẫn hoạt động như trước.

3. Ví dụ encapsulation tốt và xấu

Ví dụ xấu: trường công khai

public class Student {
    public String name;
    public int age;
}

Vấn đề của cách tiếp cận này:

  • Bất kỳ mã nào cũng có thể gán cho các trường các giá trị tùy ý, kể cả không hợp lệ.
  • Không thể thêm kiểm tra dữ liệu.
  • Nếu bạn quyết định thay đổi kiểu hoặc cấu trúc của trường, sẽ phải sửa toàn bộ mã đang dùng nó.

Ví dụ tốt: trường private và phương thức public

public class Student {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        // Có thể thêm kiểm tra!
        if (name == null || name.isEmpty()) {
            throw new IllegalArgumentException("Tên không được để trống");
        }
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        // Kiểm tra tuổi không âm
        if (age < 0) {
            throw new IllegalArgumentException("Tuổi không thể âm");
        }
        this.age = age;
    }
}

Ưu điểm:

  • Mã bên ngoài không thể thay đổi trực tiếp các trường — chỉ thông qua phương thức.
  • Có thể thêm kiểm tra, log, hành động tự động (ví dụ cập nhật thống kê).
  • Nếu biểu diễn bên trong thay đổi (ví dụ tuổi được lưu ở định dạng khác), giao diện bên ngoài vẫn giữ nguyên.

Trông như thế nào khi sử dụng

Student s = new Student();
s.setName("Alisa");
s.setAge(20);

System.out.println(s.getName() + ", tuổi: " + s.getAge());

Hãy thử gán tuổi âm — bạn sẽ nhận lỗi ngay lúc chạy! Chương trình của bạn được bảo vệ khỏi những thao tác “ngớ ngẩn”.

4. Mối liên hệ với các nguyên lý OOP khác

Encapsulation là “mẹ” của các nguyên lý OOP khác. Nếu không có nó thì không có kế thừa, đa hình hay trừu tượng. Chúng ta sẽ học kỹ sau, còn bây giờ lược qua:

  • Kế thừa (extends) cho phép tạo lớp mới dựa trên lớp đã có, mở rộng hoặc thay đổi hành vi. Nếu “ruột gan” của lớp bị mở toang, lớp con có thể vô tình làm hỏng thứ gì đó quan trọng.
  • Đa hình (khả năng các đối tượng thuộc những lớp khác nhau phản hồi cùng một thông điệp theo cách khác nhau) là không thể nếu không có ranh giới rõ ràng giữa hiện thực bên trong và giao diện bên ngoài.
  • Trừu tượng — chỉ giữ lại những đặc trưng cốt lõi và ẩn chi tiết. Encapsulation giúp hiện thực hóa trừu tượng trong thực tiễn.

Ẩn dụ

Hãy tưởng tượng một chiếc ô tô. Tài xế chỉ có vô lăng, bàn đạp và các cần gạt — đó là giao diện. Mọi thứ khác (động cơ, hộp số, điện tử) — ẩn dưới nắp capo. Nếu tài xế có thể điều khiển trực tiếp từng con ốc của động cơ, tai nạn hẳn sẽ xảy ra thường xuyên hơn nhiều!

5. Ví dụ thực tiễn: encapsulation trong ứng dụng của chúng ta

Hãy tiếp tục phát triển ứng dụng học tập — chẳng hạn “Sổ địa chỉ”. Giả sử có lớp Contact lưu tên và số điện thoại.

Không có encapsulation (phản ví dụ):

public class Contact {
    public String name;
    public String phone;
}

Cách dùng:

Contact contact = new Contact();
contact.name = ""; // Ôi! Tên rỗng
contact.phone = null; // Chưa đặt số điện thoại

Có encapsulation (cách đúng):

public class Contact {
    private String name;
    private String phone;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("Tên liên hệ không được để trống");
        }
        this.name = name;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        if (phone == null || phone.isBlank()) {
            throw new IllegalArgumentException("Số điện thoại không được để trống");
        }
        this.phone = phone;
    }
}

Bây giờ mã bên ngoài sẽ không thể để tên hoặc số điện thoại trống:

Contact contact = new Contact();
contact.setName("Ivan");
contact.setPhone("+1-999-123-45-67");

Nếu cố gắng đặt tên trống — chương trình sẽ ném lỗi.

6. Những điểm hữu ích

Encapsulation và bảo trì mã dài hạn

Khi bạn làm việc trên một dự án học tập nhỏ, có thể nghĩ rằng “làm đại cũng được”: ai mà lại đi gán tuổi âm hoặc để tên trống chứ? Nhưng ngay khi dự án lớn hơn, có thêm lập trình viên khác, và ngay cả bạn sau vài tháng cũng quên chi tiết hiện thực — lúc đó encapsulation cứu bạn khỏi hỗn loạn.

  • Dễ thay đổi “ruột” lớp — nếu cần lưu số điện thoại như một đối tượng kiểu PhoneNumber thay vì chuỗi, bạn chỉ việc đổi hiện thực, không đụng đến mã bên ngoài.
  • Dễ kiểm thử hơn — nếu mọi thay đổi chỉ diễn ra qua các phương thức, bạn dễ theo dõi dữ liệu thay đổi khi nào và ở đâu.
  • Ít lỗi hơn — bảo vệ khỏi giá trị không hợp lệ và thay đổi vô tình.

Câu hỏi: có luôn cần getter và setter không?

Nhiều người mới nghĩ: “Vì encapsulation là trường private và getter/setter public, vậy phải làm getter và setter cho mọi trường!”. Không hẳn vậy.

  • Đôi khi trường chỉ nên cho phép đọc (ví dụ định danh duy nhất). Khi đó chỉ tạo getter.
  • Đôi khi trường không cần “đưa ra ngoài” — vậy thì đừng tạo getter hay setter.
  • Có thể làm setter là private nếu chỉ được phép thay đổi bên trong lớp.

Quy tắc vàng: chỉ mở những dữ liệu và phương thức thực sự cần cho mã bên ngoài.

Hình minh họa: so sánh các cách tiếp cận

Cách tiếp cận Ví dụ truy cập trường Khả năng kiểm soát Mức độ an toàn
trường public
obj.field = value;
Không Thấp
trường private + phương thức
obj.setField(value);
Cao

7. Lỗi thường gặp khi làm việc với encapsulation

Lỗi 1: Tất cả trường của lớp đều khai báo public. Đây là lỗi phổ biến nhất ở người mới. Mã như vậy nhanh chóng trở nên khó kiểm soát: ai cũng có thể thay đổi bất cứ dữ liệu nào mà bạn không hay biết. Đừng làm vậy — kể cả khi bạn rất muốn tiết kiệm thời gian!

Lỗi 2: Getter và setter không có kiểm tra hay logic. Nếu bạn tạo các phương thức truy cập, hãy dùng chúng để kiểm tra hợp lệ: đừng cho phép gán giá trị không đúng. Chỉ “chép” giá trị từ tham số vào trường không phải lúc nào cũng là lựa chọn tốt.

Lỗi 3: Tiết lộ cấu trúc bên trong quá sớm. Nếu bạn làm sẵn getter/setter cho mọi trường “để dành”, bạn có nguy cơ mở quá nhiều chi tiết, về sau sẽ khó thay đổi.

Lỗi 4: Trả về trực tiếp các đối tượng có thể thay đổi. Nếu một trường là đối tượng có thể thay đổi (ví dụ danh sách), đừng trả về trực tiếp qua getter. Tốt hơn là trả về bản sao hoặc làm cho nó bất biến.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION