DEV Community

Class — Encapsulation

Encapsulation: giấu state sau một interface có kiểm soát

Encapsulation là việc gói state cùng các thao tác hợp lệ lên nó vào một đơn vị, và chỉ cho phép thay đổi state qua các method được kiểm soát — không cho code ngoài chạm thẳng vào dữ liệu nội bộ. Mục đích không phải "giấu cho có" mà là bảo vệ bất biến (invariant): một tài khoản ngân hàng không bao giờ âm, một đơn hàng đã giao không bị sửa item. Khi state public và ai cũng sửa được, không có chỗ nào đảm bảo các bất biến đó, và bug "dữ liệu ở trạng thái không thể xảy ra" xuất hiện.

Cơ chế hoạt động

Access modifier (private, protected, public) và private field (#) giới hạn ai truy cập được state; thay đổi đi qua method để có chỗ kiểm tra bất biến:

class BankAccount {
  #balance: number // private thật ở runtime (# field), không chỉ lúc compile
  constructor(initial: number) {
    if (initial < 0) throw new Error('số dư ban đầu không âm')
    this.#balance = initial
  }
  get balance() { return this.#balance } // đọc qua getter, không cho ghi thẳng
  withdraw(amount: number) {
    if (amount <= 0) throw new Error('số tiền không hợp lệ')
    if (amount > this.#balance) throw new Error('không đủ số dư') // bảo vệ bất biến
    this.#balance -= amount
  }
}
Enter fullscreen mode Exit fullscreen mode

#balance là private ở runtime (khác private của TS chỉ kiểm tra lúc compile và vẫn truy cập được qua JS). Mọi thay đổi đi qua withdraw/deposit, nơi bất biến "số dư không âm" được kiểm tra. Không ai set balance = -100 từ ngoài được.

Vấn đề gặp trong production

Failure mode: state public bị đặt vào trạng thái không hợp lệ. Khi field public, bất kỳ đoạn code nào cũng sửa được, và không có điểm nào canh bất biến:

// state public -> không gì ngăn trạng thái vô lý
order.status = 'shipped'
order.items = [] // đơn đã giao nhưng không có item — trạng thái không thể xảy ra trong thực tế
Enter fullscreen mode Exit fullscreen mode

Bug này khó truy vì lỗi nằm ở nơi sửa state sai, nhưng triệu chứng xuất hiện ở nơi đọc state về sau, cách xa nhau. Encapsulation gom mọi đường thay đổi về một chỗ (method), nên khi state sai, chỉ cần xem các method — không phải toàn bộ codebase.

Failure mode: God Object (over-encapsulation sai hướng). Phản ứng quá đà với encapsulation là nhồi mọi thứ vào một class khổng lồ "quản lý tất cả" — một UserManager 2000 dòng vừa giữ state, vừa gọi DB, vừa gửi email, vừa format. Đây vẫn là thiết kế tồi: class quá nhiều trách nhiệm thì khó test, khó đổi, và "đóng gói" trở thành "đống hỗn độn có tường rào". Encapsulation tốt là mỗi đơn vị giữ một phần state mạch lạc với các thao tác của riêng nó, không phải một class ôm tất cả.

Failure mode: getter/setter vô nghĩa. Tạo getter/setter chỉ để đọc/ghi thẳng field (set balance(v) { this.#balance = v }) là encapsulation hình thức — nó cho cảm giác an toàn nhưng không bảo vệ bất biến nào, vì setter cho phép set bất kỳ giá trị nào. Setter chỉ có giá trị khi nó kiểm tra gì đó; nếu không, hoặc để field readonly, hoặc dùng method có tên nghiệp vụ (withdraw) thay vì setter trần.

Cách debug và monitor

Khi điều tra "dữ liệu ở trạng thái không thể xảy ra" (đơn đã giao mà rỗng item, số dư âm), đầu tiên xác định state đó có được encapsulate không: nếu field public và sửa được khắp nơi, điểm gây lỗi nằm rải rác và khó tìm; nếu đi qua method, đặt kiểm tra/log ngay trong method là khoanh được. Đặt assertion bất biến trong các method thay đổi state (throw khi vi phạm) để fail ngay tại nguồn thay vì lan đi. Dấu hiệu God Object cần refactor: một class quá dài, quá nhiều dependency, test phải mock hàng chục thứ — đó là lúc tách theo trách nhiệm.

Tradeoff

Encapsulation làm code rõ ràng và an toàn hơn — bất biến được bảo vệ tại một chỗ, thay đổi state có kiểm soát, dễ suy luận về trạng thái hợp lệ. Cái giá là dễ overdesign: thêm class/getter/setter cho cả những thứ không cần, hoặc dồn quá nhiều vào một class thành God Object. Quy tắc thực tế: encapsulate state có bất biến cần bảo vệ (số dư, trạng thái đơn) sau method kiểm tra; với dữ liệu thuần không bất biến (DTO, value object đơn giản) thì một struct/record công khai readonly là đủ, không cần class nặng. Đóng gói để bảo vệ quy tắc, không phải để có lớp vỏ.

Câu hỏi phỏng vấn

Encapsulation mang lại gì, và # private field khác private của TypeScript thế nào?

Encapsulation gói state cùng các thao tác hợp lệ vào một đơn vị và buộc mọi thay đổi đi qua method có kiểm soát, nhờ đó bất biến của dữ liệu (số dư không âm, đơn đã giao không rỗng item) được bảo vệ tại một chỗ duy nhất — khi state sai, chỉ cần xem các method thay vì cả codebase. #balance là private thật ở runtime: không thể truy cập từ ngoài class kể cả bằng JavaScript thuần; còn private của TypeScript chỉ kiểm tra lúc compile và bị xóa khi biên dịch, nên ở runtime vẫn truy cập được qua JS. Điểm ăn điểm: cảnh báo hai cái bẫy — God Object (nhồi mọi trách nhiệm vào một class) và getter/setter vô nghĩa (không kiểm tra gì thì không bảo vệ bất biến); chỉ encapsulate state thật sự có bất biến cần giữ.

Hands-on

Xây một BankAccount với #balance private, các method deposit/withdraw kiểm tra bất biến (không âm, số tiền hợp lệ), và một getter chỉ-đọc cho số dư; thử set thẳng số dư từ ngoài để xác nhận không làm được, và thử rút quá số dư để thấy bất biến chặn lại. So sánh với một phiên bản state public và cố tình đặt trạng thái vô lý để thấy không gì ngăn. Sau đó lấy một class "manager" thật quá lớn trong một codebase, liệt kê các trách nhiệm nó ôm, và tách thành các đơn vị nhỏ mỗi cái giữ một phần state mạch lạc — đo lại độ khó của test (số mock cần) trước và sau.

Top comments (0)