Series Clean Code Thực chiến #2: Nghệ thuật thiết kế Hàm – Nhỏ, Nhỏ nữa, Nhỏ mãi!
1. Nguyên tắc tối thượng: Kích thước phải NHỎ!
Quy tắc đầu tiên khi viết hàm là nó phải nhỏ. Quy tắc thứ hai là nó phải nhỏ hơn nữa.
Một hàm dài 100 dòng chứa đầy các vòng lặp foreach, lệnh if/else lồng nhau 3-4 cấp là một tội ác với người đọc. Theo Uncle Bob, một hàm lý tưởng hiếm khi dài quá 20 dòng. Nó chỉ nên làm đủ nhiệm vụ để bao quát nội dung trên một màn hình máy tính mà không cần cuộn chuột.
Làm sao để hàm nhỏ lại? Hãy Extract (Tách) nó ra thành các hàm con. Nếu bạn thấy mình đang viết comment để giải thích một khối code gồm 5 dòng bên trong một hàm, hãy cắt 5 dòng đó ra thành một hàm mới và lấy chính cái comment đó làm tên hàm!
2. Do One Thing (Chỉ làm MỘT việc duy nhất)
Đây là quy tắc khó tuân thủ nhất nhưng lại mang tới giá trị cao nhất.
Hàm chỉ nên làm một việc. Nó phải làm tốt việc đó. Và nó chỉ được phép làm việc đó.
Hãy xem xét kịch bản hành khách quẹt thẻ qua cổng Metro.
Code "Ôm đồm" (Làm nhiều việc):
public function processTapCard(Request $request) {
// 1. Tìm thẻ
$card = Card::find($request->card_id);
// 2. Kiểm tra thẻ hợp lệ
if ($card->status !== 'active' || $card->is_locked) {
throw new Exception("Thẻ không hợp lệ");
}
// 3. Kiểm tra số dư và trừ tiền
if ($card->balance < 15000) {
throw new Exception("Không đủ số dư");
}
$card->balance -= 15000;
$card->save();
// 4. Ghi Log Database
Transaction::create([
'card_id' => $card->id,
'amount' => 15000,
'station_id' => $request->station_id
]);
// 5. Gửi lệnh mở cổng Turnstile
Http::post('http://local-turnstile/open');
return true;
}
Hàm trên vi phạm nghiêm trọng quy tắc "Do One Thing". Nó vừa làm nhiệm vụ Validation, vừa xử lý Logic tính toán (Business Logic), vừa tương tác Database, vừa gọi External API.
Clean Code (Tách theo mức độ trừu tượng):
public function processTapCard(Request $request) {
$card = Card::findOrFail($request->card_id);
$this->validateCardCanTap($card);
$this->chargeFare($card, self::BASE_FARE, $request->station_id);
$this->openTurnstile();
return true;
}
// Các hàm con sẽ che giấu chi tiết (Encapsulation)
private function validateCardCanTap(Card $card) {
if (!$card->isActive() || !$card->hasEnoughBalance(self::BASE_FARE)) {
throw new Exception("Thẻ không đủ điều kiện quẹt.");
}
}
Giờ đây, hàm processTapCard đọc như một bài văn: Lấy thẻ -> Kiểm tra điều kiện -> Trừ tiền -> Mở cổng. Người đọc không cần quan tâm chi tiết trừ tiền thế nào (update cột nào trong DB), trừ khi họ chủ động bấm vào hàm chargeFare() để xem.
3. Số lượng tham số đầu vào (Arguments)
Số lượng tham số lý tưởng của một hàm là 0 (Niladic). Tiếp theo là 1 (Monadic), theo sau là 2 (Dyadic). Hàm có 3 tham số (Triadic) nên tránh bằng mọi giá, và nếu lớn hơn 3, bạn phải có lý do cực kỳ đặc biệt.
Tại sao phải sợ tham số? Vì mỗi tham số thêm vào sẽ làm độ khó của việc Unit Test tăng lên theo cấp số nhân (bạn phải test các tổ hợp chéo của chúng).
Xấu:
public function createTransaction($cardId, $amount, $stationId, $type, $isOffline) { ... }
Tốt (Dùng Object/DTO để gom nhóm):
Nếu một hàm cần quá nhiều tham số, nghĩa là các tham số đó đang thuộc về một khái niệm chung. Hãy gom chúng thành một Đối tượng (Object) hoặc Mảng.
// Dùng Data Transfer Object (DTO)
public function createTransaction(TransactionDTO $transactionData) { ... }
4. Không có Tác dụng phụ (No Side Effects)
Tác dụng phụ là những lời nói dối ngầm trong code. Tên hàm nói là nó làm việc A, nhưng ngầm bên dưới nó lại lén lút làm thêm việc B.
Ví dụ: Bạn có một hàm tên là checkPassword(). Bạn hy vọng nó chỉ trả về true hoặc false. Nhưng người dev trước lại viết thêm đoạn logic: Nếu đúng pass thì khởi tạo luôn Session đăng nhập (Session::put).
Thế là một ngày đẹp trời, bạn chỉ muốn gọi hàm checkPassword() để xác thực quyền xóa tài khoản, vô tình nó lại reset luôn phiên đăng nhập của người dùng.
Quy tắc: Tên hàm hứa làm gì, thì chỉ làm đúng việc đó. Khai báo rõ ràng, không làm các hành động lén lút (đổi trạng thái toàn cục, thay đổi biến truyền vào theo kiểu reference).
Viết code chạy được thì nhanh, nhưng viết code để đọc được thì cần sự tỉ mỉ. Việc tách hàm nhỏ và chia nhỏ trách nhiệm ban đầu có vẻ tốn thời gian, nhưng nó sẽ cứu bạn khỏi những đêm thức trắng debug khi hệ thống phình to lên gấp 10 lần. Dọn dẹp mã nguồn cũng giống như dọn dẹp nhà cửa vậy, làm mỗi ngày một chút thì không bao giờ phải chịu cảnh rác ngập lên tận cổ.
All Rights Reserved