99 Ngày Java — Ngày 13: Đóng gói (Encapsulation)
Ngày 12 khép lại bằng một cảnh báo: constructor gác cửa lúc khai báo, nhưng field public thì sau đó ai cũng an.diemTB = -99 được. Hôm nay ta học trụ cột đầu tiên trong bốn trụ cột OOP: đóng gói (encapsulation) — giấu dữ liệu sau hàng rào private, chỉ mở những cánh cửa có kiểm soát. Nghe như "giấu giếm", thực chất là bảo vệ bất biến nghiệp vụ: số dư không âm, điểm trong thang 0–10 — những luật mà không một dòng code nào bên ngoài được phép phá.

Vấn đề: field phơi trần
public class TaiKhoan {
double soDu; // field PHƠI TRẦN — ai cũng sờ được
}
// Ở một nơi rất xa trong dự án...
taiKhoan.soDu = -5_000_000; // ✓ compile ngon lành — thảm họa!
taiKhoan.soDu *= 100; // "tự thưởng" — không ai ngăn được
// Constructor Ngày 12 gác cửa lúc KHAI SINH...// ...nhưng sau đó field vẫn là bãi đất hoang: ai đi qua cũng sửa được.// Bug kiểu này KHÓ LẦN: soDu sai ở phút 90 — ai gán? ở đâu? lúc nào?Điểm chí mạng không phải "có thể gán sai" — mà là không truy được ai gán. Field public bị gán từ bất kỳ đâu trong dự án; khi dữ liệu hỏng, hiện trường không dấu vết. Nguyên tắc rút ra: kẻ nào giữ dữ liệu, kẻ đó chịu trách nhiệm bảo vệ dữ liệu — và chỉ class chứa field mới biết luật nghiệp vụ của field đó.
private + cửa có kiểm soát
public class TaiKhoan {
private double soDu; // private: CHỈ code trong class này thấy
public TaiKhoan(double soDuBanDau) {
if (soDuBanDau < 0) throw new IllegalArgumentException("Số dư âm!");
this.soDu = soDuBanDau;
}
public double getSoDu() { // getter: cửa ĐỌC — read-only từ ngoài
return soDu;
}
public void napTien(double soTien) { // KHÔNG có setSoDu bừa bãi —
if (soTien <= 0) throw new IllegalArgumentException("Tiền nạp phải > 0");
soDu += soTien; // chỉ có nghiệp vụ CÓ KIỂM SOÁT
}
public void rutTien(double soTien) {
if (soTien > soDu) throw new IllegalStateException("Không đủ số dư");
soDu -= soTien;
}
}
// taiKhoan.soDu = -5_000_000; ✗ lỗi biên dịch: soDu has private access// taiKhoan.rutTien(999_999_999) ✗ chặn ngay tại cửa — kèm thông điệp rõ ràngCông thức ba bước: khóa field (
private) → mở cửa đọc (getter) → mở nghiệp vụ có kiểm tra (napTien/rutTien— không phảisetSoDu!).Vi phạm giờ là lỗi biên dịch — rẻ nhất trong mọi loại lỗi (Ngày 05 đã xếp hạng: compile error < runtime error < bug âm thầm).
Mọi thay đổi
soDugiờ đi qua hai cánh cửa duy nhất — đặt breakpoint hai chỗ là tóm được mọi thủ phạm; so với "tìm khắp dự án ai gán field này", một trời một vực.Để ý
rutTiennémIllegalStateException(trạng thái không cho phép) còn tham số sai némIllegalArgumentException— phân loại exception kỹ hơn ở Ngày 41.
Bốn mức access modifier
// 4 mức access modifier — từ kín nhất đến mở nhất://// private chỉ TRONG class → mặc định cho FIELD// (không ghi gì) trong cùng PACKAGE → "package-private"// protected package + lớp CON → hẹn Ngày 14 (kế thừa)// public MỌI NƠI → cho API bạn muốn công khai//// Chiến lược: KÍN NHẤT CÓ THỂ, nới dần khi thật sự cần// - field: private (gần như luôn luôn)// - method nghiệp vụ cho bên ngoài: public// - method phụ trợ nội bộ: private — đổi thoải mái, không ai phụ thuộcBạn đã gặp đủ bộ: public class từ Ngày 02, private hôm nay, còn protected sẽ có đất diễn khi kế thừa xuất hiện ngày mai. Kim chỉ nam chỉ một câu: mặc định đóng, mở có chủ đích — mỗi thứ public là một lời hứa bạn phải giữ mãi mãi, vì người khác sẽ phụ thuộc vào nó.JavaBean — quy ước getter/setter
// Quy ước JavaBean — bộ mặt quen thuộc của đóng gói:public class SinhVien {
private String ten;
private double diemTB;
public String getTen() { return ten; }
public void setTen(String ten) { this.ten = ten; }
public double getDiemTB() { return diemTB; }
public void setDiemTB(double diemTB) {
if (diemTB < 0 || diemTB > 10) // setter = TRẠM KIỂM SOÁT
throw new IllegalArgumentException("Điểm 0–10!");
this.diemTB = diemTB;
}
public boolean isXuatSac() { return diemTB >= 9.0; } // boolean → isXxx
}
// Framework (Jackson, JPA, Spring...) nhận diện thuộc tính qua get/set/is// NHƯNG đừng phản xạ getter/setter cho MỌI field:// - setter nào không có lý do nghiệp vụ → ĐỪNG viết (soDu không có setSoDu!)// - không setter nào cả = lớp bất biến — hướng đi hiện đại, hẹn Ngày 20 & 29Chuẩn đặt tên
getX/setX/isXkhông chỉ cho người đọc — cả hệ sinh thái framework (Jackson serialize JSON ở series Spring, JPA, thư viện mapping...) đều dò thuộc tính qua đúng quy ước này.Getter/setter vô tội vạ = đóng gói giả:
privatefield +setXtrống rỗng cho mọi field thì chẳng khác gì field public. Câu hỏi đúng cho từng field: "bên ngoài có lý do nghiệp vụ nào để sửa nó không?" — không có thì không setter.Getter cũng không bắt buộc phải "trần":
isXuatSac()là thuộc tính tính toán — bên ngoài không cần biết nó suy ra từdiemTB.IDE sinh getter/setter một phím, và Ngày 20 bạn sẽ gặp
record— cách Java hiện đại xóa sổ hẳn đống boilerplate này cho dữ liệu bất biến.
Bài tập nhỏ
Khóa
privatetoàn bộ field của classSach(Ngày 11–12) — thêm getter tất cả, nhưng cân nhắc từng setter: giá sách có nênsetGiatự do, hay chỉgiamGia(phanTram)có kiểm tra?Viết
TaiKhoanhoàn chỉnh như bài — thêmchuyenKhoan(TaiKhoan nhan, double soTien)dùngrutTien+napTienđể luật không bị lách.Class
DongHoDem(counter) chỉ cótang()vàgetGiaTri()— chứng minh không cách nào làm nó đếm lùi.Lấy một class cũ bạn từng viết field public — refactor sang đóng gói, đếm xem phát hiện thêm bao nhiêu chỗ lẽ ra phải kiểm tra dữ liệu.
Kết luận
Trụ cột thứ nhất đã dựng: đóng gói = private field + cửa có kiểm soát, bốn mức modifier với chiến lược kín nhất có thể, quy ước JavaBean để bắt tay framework — và tinh thần quan trọng nhất: class tự bảo vệ luật nghiệp vụ của mình, không phó mặc cho sự tử tế của code bên ngoài. Ngày 14 ta lên trụ cột thứ hai: kế thừa (inheritance) — extends, super, override, và protected sẽ có câu trả lời trọn vẹn. Hẹn gặp lại!
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


