Backend

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

SSite Admin
9 tháng 08, 2026 6 phút đọc 61 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 Java — Ngày 58: Optional

Optional là công cụ cho kiểu trả về — không field, không tham số, không bọc collection; chuỗi map/flatMap/filter thay kim tự tháp if; orElse luôn tính tham số còn orElseGet lười, orElseThrow cho “không có là lỗi” — và các anti-pattern isPresent + get, orElse(null).

23 thg 9, 20269 phút9
99 Ngày Spring — Ngày 58: Spring Boot Actuator

Boot 3 mặc định chỉ lộ health; bản đồ endpoint an toàn và nguy hiểm; HealthIndicator tự viết với liveness tách khỏi readiness cho Kubernetes, info từ build và git, metrics Micrometer với tag hữu hạn — và ba lớp khóa: cổng riêng, SecurityFilterChain với EndpointRequest, exclude env/heapdump/threaddump.

23 thg 9, 202610 phút5
So sánh EasyExcel và Apache Fesod: khác biệt thật nằm ở đâu?

Fesod là hậu duệ của EasyExcel, cùng engine SAX/SXSSF. Khác biệt kiểm chứng được: vòng đời dự án, POI 5.2.5 vs 5.5.1, các API mới (numRows, csv(), headerMergeStrategy) và độ bền với file xấu — không phải hiệu năng gấp nhiều lần.

23 thg 9, 20268 phút1