Backend

99 Ngày Java — Ngày 13: Đóng gói (Encapsulation)

SSite Admin
9 tháng 08, 2026 6 phút đọc 19 lượt xem
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á.

Sketchnote Ngày 13: đóng gói trong Java — private, getter/setter, access modifiers và JavaBean

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àng
  • Cô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ải setSoDu!).

  • 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 soDu giờ đ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.

  • Để ý rutTien ném IllegalStateException (trạng thái không cho phép) còn tham số sai ném IllegalArgumentException — 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ộc

Bạ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 & 29
  • Chuẩn đặt tên getX/setX/isX khô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ả: private field + setX trố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 private toàn bộ field của class Sach (Ngày 11–12) — thêm getter tất cả, nhưng cân nhắc từng setter: giá sách có nên setGia tự do, hay chỉ giamGia(phanTram) có kiểm tra?

  • Viết TaiKhoan hoàn chỉnh như bài — thêm chuyenKhoan(TaiKhoan nhan, double soTien) dùng rutTien + napTien để luật không bị lách.

  • Class DongHoDem (counter) chỉ có tang()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!

S

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.

Bài viết liên quan

99 Ngày Spring — Ngày 14: Bean Validation — @Valid & bộ constraint chuẩn

Ngày 14 của 99 Ngày Spring: Bean Validation khai báo luật ngay trên DTO — @Valid kích hoạt, bộ constraint chuẩn (@NotBlank vs @NotEmpty vs @NotNull, @Size, @Email, @Min/@Max), bẫy int vs Integer, validate object lồng nhau không tự lan, và lỗi 400 gom một lượt.

10 thg 8, 20265 phút0
99 Ngày Java — Ngày 14: Kế thừa (Inheritance)

Ngày 14 của 99 Ngày Java: extends trao gia tài từ lớp cha, override với @Override và super., thứ tự khai sinh cha trước con sau qua super(...), protected trả nợ Ngày 13, và phép thử is-a vs has-a — khi nào nên composition thay vì kế thừa.

10 thg 8, 20266 phút0
99 Ngày Spring — Ngày 13: @RequestBody & DTO — đừng công khai entity ra API

Ngày 13 của 99 Ngày Spring: @RequestBody + Jackson deserialize JSON thành object, DTO là hợp đồng API — chặn lỗ hổng mass assignment chiều vào và rò rỉ dữ liệu chiều ra, request/response DTO khác nhau là bình thường, và Jackson annotations tinh chỉnh hợp đồng JSON.

9 thg 8, 20266 phút9