Nguyen Le PhongNguyen Le Phong

Vì sao đọc code thường mệt hơn viết code?

Đọc code là lần theo cách hệ thống vận hành, lịch sử và những ràng buộc không nằm trên màn hình. Đi từng luồng, dựa vào test và để lại bối cảnh giúp ta hiểu trước khi vội viết lại.

Con trỏ dừng ở một function trông chẳng có gì ghê gớm. Nó nhận user, plan và date. Tên function nói một đằng, test gợi ra một nẻo, còn comment từ hai năm trước nhắc đến một ngoại lệ dành cho enterprise mà chẳng ai nhớ rõ. Chưa viết thêm dòng nào, bạn đã thấy đầu nặng trĩu. Đọc code nhiều lúc giống bước vào phòng họp sau khi mọi người về hết, rồi cố dựng lại cả cuộc tranh luận từ vài nét còn sót trên bảng.

Viết code dễ chịu ở một điểm: ta khởi đầu từ ý định của chính mình. Bài toán có thể khó, nhưng hình dạng đầu tiên vẫn nằm trong đầu ta. Khi đọc code, ý định thuộc về người khác, thường là nhiều người qua nhiều năm. Thứ cần đọc không chỉ có syntax. Trong đó còn lẫn deadline, incident, thói quen đặt tên, constraint cũ và những thỏa hiệp chưa từng được ghi thành tài liệu.

Một người nhìn vào laptop và màn hình hiển thị code, phía sau là bảng trắng có nhiều sơ đồ mờ.
Đọc code cần thời gian gom manh mối; viết lại ngay chỉ khiến phần chưa hiểu bị che dưới một cấu trúc mới.

Bởi vậy, một engineer nhiều kinh nghiệm vẫn có thể đi rất chậm trong codebase lạ. Không phải họ quên cách lập trình, mà vì hiểu luôn nặng hơn gõ. Tính năng cần sửa có khi chỉ nằm trong một file, nhưng để biết đúng file ấy có thể phải lần route, đọc test, xem migration, tìm pull request cũ và hỏi vì sao một nhánh điều kiện kỳ quặc còn tồn tại. Phần việc này hầu như vô hình, cho đến khi nó ngăn ta sửa nhầm.

Code còn giấu cả thời gian. Một nhánh điều kiện trông vô lý cho tới khi biết nó được thêm sau sự cố của khách hàng. Hai hàm phụ trợ gần giống nhau có thể không phải do cẩu thả, mà vì hai luồng đang được tách dần. Một abstraction chưa xuất hiện có khi là lựa chọn tỉnh táo sau lần nhóm từng làm quá tay và phải trả giá. Thiếu lịch sử, dòng nào cũng dễ bị đem ra phán. Có lịch sử, nhiều dòng lại trở thành dấu vết của một đánh đổi.

Cám dỗ lớn nhất là viết lại quá sớm. Cấu trúc do mình viết đương nhiên dễ hiểu với mình, nên cảm giác nhẹ đến rất nhanh. Nhưng nó sẽ đắt nếu cách vận hành mà code cũ đang bảo vệ vẫn chưa được nhìn ra. Legacy code thường cất những quy tắc nghiệp vụ li ti ở nơi không ai ngờ. Nhánh tưởng thừa có thể giữ contract với client cũ. Null check nhìn vụng có thể đang chặn một production bug chỉ xuất hiện ở dữ liệu import từ partner.

Test giúp biến việc đọc thành cuộc đối thoại với bằng chứng. Một test tốt cho biết code đã hứa điều gì, thay vì chỉ cho thấy nó đang được cài đặt ra sao. Nếu khu vực ấy chưa có test, viết một characterization test nhỏ là bước đi khiêm tốn mà chắc chắn: trước khi sửa, ghi lại cách vận hành hiện tại. Test không làm code xấu tự nhiên đẹp lên, nhưng nó đặt một tấm sàn để lần thay đổi tiếp theo bớt rơi tự do.

Một người ghi sơ đồ vào sổ trước laptop hiển thị code, quanh bàn có giấy vẽ luồng và sticky note.
Ghi lại đường đi của dữ liệu và kết quả đã kiểm chứng giúp codebase dần biến thành một bản đồ có thể lần theo.

Thay vì cố nuốt cả codebase một lúc, hãy lần theo một luồng từ đầu đến cuối. Bắt đầu ở thao tác của người dùng, API request, event hoặc failing test. Đi theo nó qua từng lớp. Ghi ra function, table, queue và service mà nó chạm tới. Đánh dấu nơi dữ liệu đổi shape, nơi error bị nuốt, nơi log xuất hiện rồi biến mất. Một khu rừng sẽ bớt đáng sợ khi ta đã tìm được một con đường thật.

Tên gọi đặc biệt quan trọng với người đọc. Một cái tên mơ hồ bắt mọi người đến sau trả thêm một khoản thuế nhỏ. Một cái tên chính xác đôi khi thay được cả đoạn giải thích. Không cần function nào cũng dài ngoằng; chỉ cần tên mang đúng khác biệt mà người đọc phải biết: draft hay submitted, calculated hay stored, display price hay billing price, tenant permission hay user permission. Đặt tên rõ là một cách nhỏ để đỡ việc cho người chưa xuất hiện.

Chú thích cũng nên phục vụ mục đích ấy. Chú thích hữu ích hiếm khi kể lại điều code đã nói. Nó giải thích vì sao đoạn code mang hình dạng này, vì sao phương án có vẻ hiển nhiên lại không dùng được, hoặc constraint bên ngoài nào đang được giữ. Một câu như partner import gửi empty string khi thiếu ngày có thể cứu ai đó khỏi “dọn” mất dòng đang âm thầm chắn lỗi. Điều đáng ghi là lý do, không phải thao tác.

Việc đọc code còn nhẹ hơn khi nhóm coi câu hỏi là tiến bộ, không phải bài thi seniority. Có người hỏi vì sao module lại được tách như vậy thì đó là dịp phục hồi shared memory. Nhiều codebase khó hiểu không hẳn vì thuật toán quá ghê gớm, mà vì bối cảnh chỉ được phép sống trong đầu vài người.

Ở đây có một kiểu kiên nhẫn rất kỹ thuật. Trước khi đổi code, học xem nó đang làm nhiệm vụ gì. Trước khi chê một dòng lạ, tìm sức ép đã tạo ra nó. Trước khi thêm abstraction, hiểu rõ phần lặp mà ta định gom. Sự kiên nhẫn này không làm nhóm chậm đi; nó ngăn mỗi lần “cải thiện” lại đắp thêm một lớp mơ hồ.

Lần tới nếu đọc code thấy khó hơn viết, đừng vội xem đó là thiếu năng lực. Độ khó ấy chính là công việc. Hãy lần theo một luồng, thêm một ghi chú, khóa một kết quả bằng test và hỏi một câu đủ cụ thể. Sự hiểu biết lớn lên qua những lượt nhỏ như vậy. Codebase cũng tử tế hơn mỗi khi người đi qua để lại nhiều bối cảnh hơn lúc họ bước vào.

Bạn thấy bài viết thế nào?