Hàm nhỏ: tách theo mức trừu tượng, không theo số dòng
"Hàm nên nhỏ" là lời khuyên hay bị hiểu thành "đếm dòng": dưới 20 dòng thì tốt, trên thì xấu. Đó là cách hiểu sai. Một hàm tốt làm một việc ở một mức trừu tượng, đọc như một câu mô tả ý định; độ dài chỉ là hệ quả. Mục tiêu thật là giảm độ phức tạp người đọc phải giữ trong đầu cùng lúc — một hàm dài trộn nhiều mức (logic nghiệp vụ lẫn thao tác chuỗi lẫn gọi I/O) buộc đọc tất cả mới hiểu được phần nào. Tách quá tay thành chục hàm tí hon lại gây vấn đề ngược: phải nhảy khắp file để ráp lại một luồng đơn giản.
Cơ chế hoạt động
Nguyên tắc cốt lõi: mỗi hàm nên ở một mức trừu tượng. Hàm cấp cao kể câu chuyện bằng các bước có tên; chi tiết của mỗi bước nằm trong hàm con cùng mức với nhau.
// trộn nhiều mức: điều phối + chi tiết tính toán + định dạng, đọc phải hiểu hết
function checkout(cart) {
let total = 0
for (const item of cart.items) total += item.price * item.qty
if (cart.coupon) total *= (1 - cart.coupon.rate)
const tax = Math.round(total * 0.1)
// ... 30 dòng nữa trộn lưu DB và gửi mail
}
// tách theo mức: hàm trên đọc như mô tả ý định
function checkout(cart) {
const subtotal = calculateSubtotal(cart.items)
const total = applyCoupon(subtotal, cart.coupon)
const order = createOrder(cart, total, calculateTax(total))
return order
}
Bản tách cho người đọc hiểu luồng mà không cần chi tiết; ai cần chi tiết mới mở calculateSubtotal. Mỗi hàm con kiểm thử được riêng.
Vấn đề gặp trong production
Failure mode: hàm dài trộn nhiều mức trừu tượng. Một hàm 200 dòng làm validation + tính toán + truy vấn + định dạng buộc người đọc giữ toàn bộ ngữ cảnh trong đầu, và mỗi sửa đổi nhỏ đụng một khối lớn nên dễ gây hồi quy. Đây là dạng khó bảo trì nhất. Tách thành các bước có tên giảm tải nhận thức và khoanh vùng thay đổi — sửa cách tính thuế chỉ đụng calculateTax.
Failure mode: quá phân mảnh (over-extraction). Phản ứng quá đà là tách mọi thứ, kể cả một biểu thức dùng một lần, thành hàm riêng:
function isPositive(n) { return n > 0 } // dùng đúng một chỗ, che mất ý đơn giản
function addOne(n) { return n + 1 }
Khi đó đọc một luồng phải nhảy qua chục hàm một-dòng rải khắp file, mỗi cú nhảy là một lần mất mạch. Hàm chỉ đáng tách khi nó đặt tên cho một ý niệm (làm rõ ý định) hoặc được dùng lại, không phải để giảm số dòng của hàm cha. Một khối ba dòng có tên rõ trong ngữ cảnh đôi khi dễ đọc hơn một lời gọi hàm phải đi tìm.
Failure mode: tách nhưng vẫn truyền/giữ state lẫn lộn. Tách hàm mà mỗi hàm con vẫn nhận mười tham số hoặc đọc/ghi biến chung thì không giảm phức tạp thật — chỉ dời nó đi. Tách tốt đi kèm dữ liệu vào/ra rõ ràng cho mỗi hàm con.
Cách debug và monitor
Dấu hiệu một hàm nên tách: phải cuộn để đọc hết, có comment kiểu // tính toán / // gửi mail chia hàm thành khúc (mỗi khúc thường là một hàm con), hoặc độ lồng sâu (nhiều tầng if/for). Công cụ đo cyclomatic complexity (qua linter) cảnh báo hàm quá phức tạp — dùng như tín hiệu, không phải luật cứng. Dấu hiệu tách quá tay: đọc một feature phải mở chục file/hàm một-dòng, hoặc tên hàm chỉ lặp lại nội dung (incrementByOne). Một câu hỏi review hữu ích: "hàm này có đọc được như một câu mô tả việc nó làm không, và có cần nhảy đi đâu để hiểu luồng chính không?"
Tradeoff
Tách hàm theo mức trừu tượng làm code dễ đọc (luồng chính như mô tả ý định), dễ test (mỗi phần riêng), dễ sửa an toàn (thay đổi khoanh vùng). Cái giá khi làm quá là phân mảnh — luồng trải khắp nhiều hàm nhỏ, đọc phải nhảy liên tục, gián đoạn mạch hiểu. Quy tắc thực tế: tách khi một hàm trộn nhiều mức trừu tượng hoặc một khối xứng đáng có một cái tên giải thích ý định; giữ lại khi tách chỉ để giảm dòng mà không thêm nghĩa. Đo bằng "đọc có hiểu ý định không", không bằng số dòng tuyệt đối — không có con số kỳ diệu nào.
Câu hỏi phỏng vấn
Một hàm nên dài bao nhiêu dòng, và khi nào tách hàm là sai?
Không có con số cố định — độ dài là hệ quả, không phải mục tiêu. Nguyên tắc đúng là mỗi hàm làm một việc ở một mức trừu tượng và đọc được như một câu mô tả ý định: hàm cấp cao kể luồng bằng các bước có tên, chi tiết của mỗi bước nằm trong hàm con cùng mức. Một hàm dài trộn nhiều mức (điều phối + tính toán + I/O + định dạng) cần tách vì nó buộc giữ toàn bộ ngữ cảnh trong đầu và mỗi sửa nhỏ đụng khối lớn gây hồi quy. Tách là sai khi over-extraction: biến mọi biểu thức dùng một lần thành hàm riêng làm luồng trải khắp file, đọc phải nhảy liên tục và mất mạch — hàm chỉ đáng tách khi đặt tên cho một ý niệm (làm rõ ý định) hoặc được tái sử dụng. Điểm ăn điểm: dùng cyclomatic complexity và comment-chia-khúc làm tín hiệu tách, và đảm bảo mỗi hàm con có dữ liệu vào/ra rõ chứ không chỉ dời state đi.
Hands-on
Lấy một hàm dài thật (kiểu checkout/processRequest 100+ dòng trộn validation, tính toán, truy vấn, định dạng) và tách hàm cấp cao thành các bước có tên ở cùng mức trừu tượng, đẩy chi tiết xuống hàm con với input/output rõ ràng; viết unit test cho một hàm con thuần (như calculateTax) để thấy nó test được độc lập. Sau đó cố tình over-extract một hàm khác thành nhiều hàm một-dòng (isPositive, addOne) và cảm nhận việc đọc luồng phải nhảy liên tục, rồi gộp lại những cái không thêm nghĩa. Chạy linter đo cyclomatic complexity trước/sau để đối chiếu tín hiệu với cảm nhận đọc.
Top comments (0)