Clean Code
09 Phần III · Cấu trúc & Độ bền Chương 10

Lớp

Classes

Code sạch không dừng ở câu lệnh và hàm — nó phải vươn tới mức tổ chức cao hơn: lớp. Chương này gói trong một mệnh lệnh: lớp phải nhỏ, đo bằng số trách nhiệm chứ không phải số dòng; mỗi lớp chỉ có một lý do để thay đổi (SRP), kết dính cao (cohesion), và được tổ chức để thay đổi không gây đau (OCP, DIP).

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 10

Khá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

naming as a measure

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ừ

the 25-word test

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:

Python · trước✗ Bẩn
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
Python · sau✓ Sạch
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

thuật toán

Chỉ lo sinh số nguyên tố — phần logic toán học thuần.

RowColumnPagePrinter

định dạng

Chỉ lo định dạng output thành trang, hàng và cột.

PrimePrinter

môi trường chạy

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 10

RAG  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:

Python · trước✗ Bẩn
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ứ
Python · sau✓ Sạch
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 10

RAG  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:

Python · trước✗ Bẩn
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
Python · sau✓ Sạch
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.

NguồnChương 10 (Classes), Clean Code: A Handbook of Agile Software Craftsmanship — Robert C. Martin, Prentice Hall 2008.