01 Trừu tượng hóa dữ liệu
Giữ biến private không chỉ là chuyện đặt getter và setter lên trên chúng.
Robert C. Martin · Chương 6 (diễn giải sát)Trừu tượng hóa (data abstraction) = phơi một giao diện trừu tượng để người dùng thao tác trên bản chất dữ liệu mà không cần biết nó được hiện thực ra sao. Phơi biến qua getter/setter một cách mù quáng không phải là trừu tượng — nó chỉ là đẩy cấu trúc nội bộ ra ngoài.
Point cụ thể
Phơi thẳng x, y public. Lộ rằng dữ liệu lưu theo hệ tọa độ chữ nhật, và buộc người dùng thao tác từng tọa độ độc lập.
Point trừu tượng
Phơi getX/getY, setCartesian, getR/getTheta, setPolar. Ẩn việc lưu theo hệ tọa độ nào, lại còn áp chính sách truy cập: đọc lẻ được, set phải set chung (nguyên tử).
RAG Ví dụ Vehicle của sách: getGallonsOfGasoline() lộ đơn vị đo, còn getPercentFuelRemaining() là trừu tượng. Trong hệ RAG, đừng phơi cách lưu ngữ cảnh — hãy phơi câu hỏi nghiệp vụ trên nó:
Vì sao: getter/setter mù quáng phơi nguyên cấu trúc lưu trữ (danh sách chunk + điểm thô); mọi nơi gọi phải tự biết "thế nào là đủ ngữ cảnh". Giao diện trừu tượng giấu hiện thực và phơi ý nghĩa — đổi cách lưu sau này không phá nơi gọi.
class RetrievedContext:
def __init__(self):
self.chunks = [] # public: lộ là 1 list
self.scores = [] # public: lộ điểm thô từng cái
# nơi dùng phải tự ghép: biết cấu trúc bên trong
ctx.chunks; ctx.scores # phơi hiện thực ra ngoài
class RetrievedContext:
def top_passages(self, k: int) -> list[str]: ... # phơi BẢN CHẤT
def is_confident(self) -> bool: ... # không lộ cách lưu
# nơi dùng nói bằng ngôn ngữ nghiệp vụ:
ctx.top_passages(5); ctx.is_confident()
02 Bất đối xứng Data/Object
Hai khái niệm này đối nghịch nhau như âm với dương — đó là lý do gọi là "bất đối xứng" (anti-symmetry):
Đối tượng
Giấu dữ liệu sau lớp trừu tượng, và phơi các hàm thao tác trên dữ liệu đó.
Cấu trúc dữ liệu
Phơi dữ liệu và không có hàm hành vi đáng kể nào.
Sách minh họa bằng bài toán hình học (Shape) viết theo hai lối. Cùng một bài toán, hai cách tổ chức ngược nhau:
| Khía cạnh | Lối thủ tục (data structure) | Lối OO (object) |
|---|---|---|
| Các lớp hình | Square/Rectangle/Circle chỉ chứa data (struct), không hàm | Mỗi lớp tự chứa area() của mình |
| Nơi đặt phép tính | Một lớp Geometry gom area(), dùng instanceof rẽ nhánh theo loại | Không cần Geometry; gọi area() đa hình trên từng hình |
| Hành vi nằm ở đâu | Tách rời khỏi dữ liệu, gom vào hàm ngoài | Gói cùng dữ liệu trong từng đối tượng |
Lưu ý đọc bảng: đây không phải bảng "tốt – xấu". Hai lối chỉ là hai cách tổ chức; ưu thế của bên này là điểm yếu của bên kia — phần kế giải thích đánh đổi.
03 Hệ quả của đánh đổi
Vì hai lối đối nghịch, ưu/nhược của chúng cũng đối nghịch. Cái khó của OO lại là cái dễ của thủ tục, và ngược lại. Đây là so sánh trung tính — không bên nào sai:
Thủ tục + cấu trúc dữ liệu
Dễ thêm HÀM mới: thêm perimeter() vào Geometry mà các lớp hình không phải đổi.
Khó thêm KIỂU mới: thêm một hình là phải sửa mọi hàm trong Geometry.
Hướng đối tượng
Dễ thêm KIỂU mới: thêm một lớp hình mới mà không đụng các hàm cũ.
Khó thêm HÀM mới: thêm một phép tính là mọi lớp hình phải đổi theo.
"Mọi thứ đều là object" là một myth. Lập trình viên trưởng thành biết: đôi khi một cấu trúc dữ liệu đơn giản cộng vài thủ tục lại phù hợp hơn. Hãy chọn cách hợp với việc, không vì thành kiến.
RAG Soi vào hệ RAG: nếu bạn đoán sẽ liên tục thêm kiểu retriever mới (vector, BM25, hybrid…) thì lối OO (mỗi retriever một lớp đa hình) êm hơn. Nếu tập kiểu cố định nhưng bạn hay thêm phép xử lý trên một bản ghi (rerank, dedup, highlight…), thì để bản ghi là cấu trúc dữ liệu phẳng rồi viết các thủ tục lại tiện hơn. Cùng một hệ, hai quyết định khác nhau — tùy trục thay đổi nào năng động hơn.
04 Luật Demeter & Train Wreck
Nói chuyện với bạn bè, đừng nói chuyện với người lạ.
Cách diễn nôm của Law of Demeter · Chương 6Luật Demeter (Law of Demeter): một module không nên biết nội thất của object mà nó thao tác. Cụ thể, method f của lớp C chỉ nên gọi method của:
Bốn nhóm được phép
chính C; object do f tạo ra; object được truyền vào f làm tham số; object giữ trong instance variable của C.
Điều cấm
Không gọi method trên object do các hàm trên trả về. Đó là nói chuyện với "người lạ".
Train Wreck — "đoàn tàu trật bánh"
Chuỗi gọi nối đuôi như đoàn toa tàu vừa khó đọc vừa ràng buộc chặt code gọi vào cấu trúc nội bộ nhiều tầng:
Vì sao: nếu ctxt/Options/ScratchDir là object thật (có hành vi) thì chuỗi này vi phạm Demeter — code gọi biết quá nhiều nội thất. (Nếu chúng chỉ là cấu trúc dữ liệu thuần, chúng vốn phơi cấu trúc nên Demeter không áp dụng — xem mục 06.) Cách chữa: tách từng bước hoặc, tốt hơn, bảo object làm hộ.
# a.getB().getC().doD() — toa nối toa, lộ 3 tầng nội thất
out_dir = ctxt.get_options().get_scratch_dir().get_absolute_path()
path = out_dir + "/" + class_file_name
stream = open(path, "wb") # code gọi phụ thuộc vào cả Options & ScratchDir
# tối thiểu: tách chuỗi, không gọi trên giá trị trả về của giá trị trả về
opts = ctxt.get_options()
scratch_dir = opts.get_scratch_dir()
path = scratch_dir.get_absolute_path() + "/" + class_file_name
# (vẫn còn lộ cấu trúc — bản vá triệt để ở mục 06: bảo ctxt làm hộ)
05 Hybrid — tệ nhất cả hai
Tệ nhất là cấu trúc nửa đối tượng nửa cấu trúc dữ liệu: vừa có hàm làm việc quan trọng, vừa phơi biến private qua getter/setter công khai như thể đó là biến public.
Vì sao: Hybrid gánh nhược điểm của cả hai phía — khó thêm hàm mới (vì có cả hành vi) lẫn khó thêm kiểu mới (vì có cả dữ liệu phơi ra). Nó là dấu hiệu của thiết kế rối hoặc thiếu hiểu biết. Hãy quyết dứt khoát: hoặc là object (giấu hết, chỉ phơi hành vi), hoặc là data structure (phơi hết, không hành vi).
class SearchSession:
def run_query(self, q): ... # hành vi quan trọng (giống object)
def get_raw_index(self): # NHƯNG lại phơi nội thất ra (giống data)
return self._index
def set_raw_index(self, idx):
self._index = idx # nửa nạc nửa mỡ → tệ cả hai
class SearchSession: # rõ ràng là OBJECT
def run_query(self, q): ... # chỉ phơi hành vi
def reindex(self, docs): ... # không phơi _index ra ngoài
# _index hoàn toàn private — không getter/setter mù quáng
06 Che giấu cấu trúc · Tell, Don't Ask
Quay lại train wreck mục 04: nếu ctxt là object có hành vi thật, đừng điều hướng xuyên qua nó. Hãy tự hỏi vì sao ta cần đường dẫn tuyệt đối của scratch dir? Câu trả lời: để tạo một scratch file tên cho trước. Vậy thì hãy bảo object làm điều đó, thay vì hỏi nó về nội thất rồi tự làm.
Vì sao — Tell, Don't Ask: bản "bẩn" lôi absolutePath ra rồi tự ghép đường dẫn và mở file — đây là Feature Envy, code gọi thèm muốn làm việc của ctxt. Bản "sạch" giao việc cho ctxt: nó giấu nội thất (Options, ScratchDir), trả thẳng thứ ta cần, và không còn vi phạm Luật Demeter.
# HỎI nội thất rồi tự ghép — lộ cấu trúc, vi phạm Demeter
abs_path = ctxt.get_options().get_scratch_dir().get_absolute_path()
file_path = abs_path + "/" + class_file_name
stream = open(file_path, "wb")
# BẢO ctxt làm hộ — nó tự giấu Options/ScratchDir bên trong
stream = ctxt.create_scratch_file_stream(class_file_name)
# tương đương Java: BufferedOutputStream bos =
# ctxt.createScratchFileStream(classFileName);
07 DTO & Active Record
DTO
Dạng tinh túy của cấu trúc dữ liệu: lớp chỉ có biến public, không hàm. Hữu ích khi nói chuyện với DB hay parse message từ socket — là tầng dịch đầu tiên biến raw data thành object. ("Bean" = biến private + getter/setter — giả-đóng-gói, thường chẳng lợi gì thêm.)
Active Record
Dạng đặc biệt của DTO: biến public (hoặc bean) cộng vài method điều hướng như save/find, dịch trực tiếp từ một bảng DB.
RAG Sai lầm phổ biến: nhét luật nghiệp vụ vào Active Record — biến nó thành Hybrid (mục 05). Trong hệ RAG, một dòng bảng documents nên ở yên là dữ liệu; logic "tài liệu này có đủ điều kiện đưa vào index không" thuộc về một object nghiệp vụ riêng:
Vì sao: coi Active Record thuần là cấu trúc dữ liệu; đặt luật nghiệp vụ vào một object riêng (object này thường giữ một instance Active Record và ẩn dữ liệu đó đi). Nhờ vậy mỗi bên giữ đúng bản chất — không sinh ra Hybrid.
class DocumentRecord: # Active Record (dịch từ bảng)
text: str # biến public
lang: str
def save(self): ... # method điều hướng — OK
def is_indexable(self): # NHÉT luật nghiệp vụ vào → thành hybrid
return self.lang == "vi" and len(self.text) > 50
class DocumentRecord: # AR thuần = cấu trúc dữ liệu
text: str; lang: str
def save(self): ...
class IndexPolicy: # object nghiệp vụ, GIẤU 1 instance AR
def __init__(self, rec): self._rec = rec
def is_indexable(self) -> bool:
return self._rec.lang == "vi" and len(self._rec.text) > 50
Ranh giới service (PHP ↔ Python): khi Laravel gửi tài liệu sang pipeline Python, payload đi qua dây chính là một DTO thuần — bên PHP cũng nên coi nó là cấu trúc dữ liệu để dịch, đừng gắn luật nghiệp vụ vào DTO truyền đi.
// DTO thuần: chỉ chở dữ liệu sang Python, KHÔNG chứa luật nghiệp vụ
final class IngestPayload {
public function __construct(
public string $docId,
public string $text,
public string $lang,
) {}
}
Http::post('http://rag-py/ingest', (array) $payload);
08 Ghi nhớ nhanh
Trừu tượng hóa ≠ getter/setter. Phơi bản chất dữ liệu, giấu cách lưu (Vehicle: getPercentFuelRemaining chứ không getGallonsOfGasoline).
Object giấu data – phơi hàm; Data structure phơi data – không hàm. Hai thứ đối nghịch, không thứ nào "đúng" tuyệt đối.
Thủ tục dễ thêm HÀM, khó thêm KIỂU; OO ngược lại. "Mọi thứ là object" là myth — chọn theo trục thay đổi năng động hơn.
Tránh Train Wreck. Đừng gọi method trên giá trị do method khác trả về (Luật Demeter) — khi đối tượng có hành vi thật.
Tránh Hybrid. Nửa object nửa data structure = tệ cả hai chiều. Quyết dứt khoát một phía.
Tell, Don't Ask. Bảo object làm hộ (createScratchFileStream) thay vì hỏi nội thất rồi tự làm.
Đừng nhét luật nghiệp vụ vào Active Record. Coi nó là data structure; tách object nghiệp vụ riêng giữ một instance AR.