01 Tổ chức lớp
Theo convention Java, một lớp đọc xuôi từ trên xuống. Trật tự khai báo có quy ước rõ ràng, để lớp "đọc như một bài báo":
Hằng public (public static constants)
Đặt trên cùng — phần ai cũng nhìn thấy trước.
Biến static private
Tiếp theo là các biến static dùng nội bộ.
Biến instance private
Rồi tới biến của từng đối tượng. Hiếm khi có lý do chính đáng để biến là public.
Public method, rồi private util ngay sau
Mỗi public method được theo bởi private utility mà nó gọi — đặt ngay bên dưới, theo Stepdown Rule.
Encapsulation (đóng gói): giữ biến và hàm tiện ích ở mức private. Đôi khi cần nới lỏng — cho protected hoặc package-level để một test truy cập được. Nhưng nới lỏng đóng gói luôn là lựa chọn cuối cùng: ta tìm cách giữ private trước đã.
02 Lớp phải nhỏ!
Luật đầu tiên của lớp là phải nhỏ. Luật thứ hai là phải nhỏ hơn thế nữa.
Robert C. Martin · Chương 10Khác với hàm — đo bằng số dòng vật lý — lớp được đo bằng trách nhiệm (responsibilities). Một lớp có thể chỉ vài dòng mà vẫn quá lớn nếu nó ôm quá nhiều việc.
Tên phải mô tả trách nhiệm
Không nghĩ ra được tên ngắn, súc tích cho lớp → lớp đang quá lớn. Các weasel words như Processor, Manager, Super là dấu hiệu gộp nhiều trách nhiệm.
Quy tắc 25 từ
Mô tả được lớp trong ~25 từ mà không dùng "if / and / or / but". Phải dùng "and" để mô tả → lớp có nhiều hơn một trách nhiệm.
God class (lớp thần thánh): ví dụ kinh điển của sách là SuperDashboard phơi ~70 public method — gánh hầu như mọi việc của ứng dụng. Ngay cả khi một lớp chỉ có 5 method, nó vẫn quá nhiều trách nhiệm nếu mô tả buộc phải dùng "and".
03 SRP — một lý do để thay đổi
Một lớp hay module chỉ nên có một, và chỉ một, lý do để thay đổi.
Robert C. Martin · Chương 10 (Single Responsibility Principle)SRP là một trong những nguyên lý OO quan trọng nhất, nhưng cũng hay bị vi phạm nhất. Nỗi lo "nhiều lớp nhỏ thì khó thấy bức tranh lớn" là có thật — nhưng một hệ nhiều lớp nhỏ không có nhiều "bộ phận chuyển động" hơn một hệ ít lớp lớn. Như hộp đồ nghề: nhiều ngăn có nhãn dễ tìm hơn vài ngăn quăng tất cả vào.
RAG SuperDashboard ôm cả 3 method quản lý phiên bản (version) lẫn mọi thứ khác. Soi vào hệ RAG: một lớp "điều phối" hay phình ra ôm luôn việc theo dõi phiên bản pipeline. Tách phần version ra lớp riêng — nó còn tái dùng được ở nơi khác:
class RagDashboard: # nhiều lý do để đổi
def run_query(self, q): ... # việc chính: điều phối truy vấn
def get_major_version(self): ... # … nhưng lại ôm cả version
def get_minor_version(self): ...
def get_build_number(self): ... # đổi cách đánh version = phải mở lớp này
class Version: # một trách nhiệm: phiên bản
def major(self): ...
def minor(self): ...
def build(self): ...
class RagDashboard: # chỉ còn lo điều phối truy vấn
def run_query(self, q): ...
Vì sao: tách 3 method version thành lớp Version cho mỗi lớp một lý do để thay đổi; RagDashboard giờ chỉ đổi khi logic điều phối đổi. Nhiều lớp nhỏ chuyên biệt thắng ít lớp lớn đa năng: dễ tìm chỗ sửa, chỉ phải hiểu phần phức tạp bị ảnh hưởng trực tiếp, và Version tái dùng được.
04 Cohesion — kết dính
Lớp nên có ít biến instance; mỗi method nên thao tác một hoặc nhiều biến đó. Kết dính (cohesion) tối đa = mỗi biến được mọi method dùng — thường không khả thi, nhưng ta muốn cohesion cao, để biến và method gắn thành một khối logic.
Khi cohesion giảm → có lớp muốn "thoát ra": giữ hàm nhỏ và tham số ngắn đôi khi làm tăng số biến instance chỉ được dùng bởi một nhóm nhỏ method. Đó chính là dấu hiệu một lớp con đang muốn tách ra — hãy tách thành lớp cohesive hơn.
Chia hàm lớn → lộ ra lớp mới
Ví dụ PrintPrimes của Knuth: một hàm lớn, lộn xộn, thụt lề sâu, nhiều biến lạ, coupling chặt. Chia nhỏ nó lộ ra ba trách nhiệm riêng biệt, mỗi cái thành một lớp cohesive:
PrimeGenerator
Chỉ lo sinh số nguyên tố — phần logic toán học thuần.
RowColumnPagePrinter
Chỉ lo định dạng output thành trang, hàng và cột.
PrimePrinter
Chỉ lo cách gọi / môi trường thực thi chương trình.
Chương trình kết quả dài hơn (tên dài hơn, thêm khai báo lớp/hàm như chú thích, thêm whitespace) — nhưng rõ ràng và tổ chức tốt hơn hẳn.
05 Tổ chức để thay đổi · OCP
Thay đổi là liên tục. Mỗi lần đổi là một rủi ro hệ thống thôi chạy đúng. Một lớp Sql sinh chuỗi SQL từ metadata, nhưng chưa hỗ trợ update — muốn thêm thì phải "mở lớp Sql ra" sửa, rủi ro phá code khác và buộc phải retest tất cả.
Lớp nên mở để mở rộng, nhưng đóng với sửa đổi.
Open–Closed Principle, diễn giải sát · Chương 10RAG Cùng vấn đề trong hệ RAG: một lớp sinh câu lệnh truy vấn store, mỗi loại lệnh mới (search / upsert / delete) lại bắt ta mở lại lớp cũ. Tách thành một abstract base + một lớp con cho mỗi lệnh:
class Sql: # thêm lệnh = MỞ lớp này ra sửa
def create(self): ...
def select(self): ...
def insert(self): ...
def _values_list(self): ... # util riêng của insert, nằm lẫn ở đây
# muốn thêm update() → phải sửa lớp Sql, retest mọi thứ
class Sql(ABC): # đóng với sửa đổi
@abstractmethod
def generate(self) -> str: ...
class CreateSql(Sql): ...
class SelectSql(Sql): ...
class InsertSql(Sql):
def _values_list(self): ... # util dời vào ĐÚNG lớp con
class UpdateSql(Sql): ... # thêm lệnh = THÊM lớp con, không sửa lớp cũ
Vì sao: tách abstract Sql với các lớp con CreateSql / SelectSql / InsertSql / UpdateSql (util riêng dời vào đúng lớp con; hành vi chung cô lập vào Where, ColumnList). Thêm UpdateSql = thêm một lớp, KHÔNG đụng lớp cũ — tôn trọng cả SRP lẫn OCP. Mỗi lớp cực đơn giản, dễ hiểu, dễ test, rủi ro phá nhau gần như bằng 0.
06 Cô lập khỏi thay đổi · DIP
Nhu cầu đổi nghĩa là code sẽ đổi. Một lớp client phụ thuộc vào chi tiết hiện thực cụ thể (concrete) sẽ gặp rủi ro khi chi tiết đó đổi. Giải pháp: phụ thuộc vào abstraction (interface / abstract class) để cô lập.
Lớp nên phụ thuộc vào abstraction, không phụ thuộc vào chi tiết cụ thể.
Dependency Inversion Principle, diễn giải sát · Chương 10RAG Ví dụ Portfolio của sách phụ thuộc trực tiếp API TokyoStockExchange (giá đổi mỗi 5 phút → rất khó test). Trong hệ RAG, một dịch vụ trả lời phụ thuộc thẳng vào một nhà cung cấp embedding cụ thể cũng vướng đúng bẫy đó. Đảo phụ thuộc qua interface rồi inject:
class Portfolio: # phụ thuộc concrete → khó test
def __init__(self):
self.exchange = TokyoStockExchange() # giá đổi mỗi 5 phút
def value(self):
total = 0
for sym, n in self.holdings.items():
total += self.exchange.current_price(sym) * n # giá biến động
return total
class StockExchange(Protocol): # abstraction
def current_price(self, symbol: str) -> Money: ...
class Portfolio:
def __init__(self, exchange: StockExchange): # INJECT qua constructor
self.exchange = exchange
def value(self): ...
# test: stub giá cố định MSFT=100 → 5 cổ kỳ vọng = 500
class FixedStockExchangeStub:
def current_price(self, symbol): return Money(100)
Vì sao: đưa interface StockExchange vào giữa, Portfolio nhận nó qua constructor (dependency injection). Test giờ chỉ cần một FixedStockExchangeStub cố định giá MSFT=100 và kỳ vọng 5 cổ = 500 — không còn phụ thuộc giá biến động. Decoupling khỏi concrete khiến hệ thống linh hoạt hơn, dễ tái dùng, dễ hiểu, và cô lập khỏi thay đổi.
07 Ghi nhớ nhanh
Tổ chức lớp: hằng public → static private → instance private → public method (private util ngay sau, theo Stepdown). Nới lỏng đóng gói chỉ để test là lựa chọn cuối cùng.
Lớp phải nhỏ — đo bằng trách nhiệm, không phải số dòng. Tên không súc tích, hay weasel words (Processor/Manager/Super), hay phải dùng "and" để mô tả ⟹ lớp quá lớn.
SRP: mỗi lớp một lý do để thay đổi. Nhiều lớp nhỏ chuyên biệt > ít lớp lớn đa năng.
Cohesion cao: method dùng nhiều biến instance. Khi cohesion tụt → tách hàm lớn để lộ lớp mới (PrimeGenerator / RowColumnPagePrinter / PrimePrinter).
OCP: mở để mở rộng, đóng với sửa đổi. Thêm hành vi = thêm lớp con, không mở lại lớp cũ.
DIP: phụ thuộc abstraction, không phụ thuộc concrete. Inject qua constructor → dễ test bằng stub.