Pull request đang mở trên màn hình, còn ly cà phê bên cạnh bàn phím thì đã nguội. Tôi đã đổi tên cùng một helper ba lần, dời một điều kiện qua hai chỗ khác nhau, rồi nhìn một function vốn đã chạy đúng như thể nó vừa làm mình thất vọng. Không có gì đang hỏng. Test đã xanh. Reviewer đang chờ. Vậy mà tôi vẫn polish tiếp, vì trong đầu tôi vẫn có một phiên bản code sạch hơn mà mình chưa chạm tới.
Trong một thời gian dài, tôi nghĩ đó là biểu hiện của sự cẩn thận. Engineer giỏi viết code elegant. Senior engineer tìm ra abstraction mà người khác bỏ sót. Phiên bản tốt nhất của một change, theo tôi khi đó, là phiên bản khi đọc vào thấy gần như hiển nhiên. Tôi vẫn tin có một vẻ đẹp trong code trở nên đơn giản sau nhiều suy nghĩ cẩn thận. Nhưng tôi không còn tin sự hoàn hảo là mục tiêu đúng.
Vấn đề của code hoàn hảo là nó rất dễ tối ưu cho cảm giác riêng của người viết. Tác giả hiểu hình dạng của nó. Tác giả nhớ tradeoff. Tác giả giải thích được vì sao abstraction này thông minh và vì sao edge case kia nằm trong một nhánh nhỏ khó thấy. Nhưng codebase không phải notebook cá nhân. Nó là một nơi làm việc chung. Câu hỏi thật không phải là đoạn code có làm tôi hài lòng ngay lúc viết hay không. Câu hỏi là người khác có thể hiểu nó, review nó, sửa nó và tin nó khi tôi không có mặt trong phòng hay không.
Tôi bắt đầu nhận ra điều này trong những khoảnh khắc code review rất nhỏ. Một đoạn code có thể compact về mặt kỹ thuật nhưng vẫn khiến reviewer phải chậm lại quá nhiều. Một refactor có thể giảm duplication nhưng lại làm feature kế tiếp khó đặt vào hơn. Một generic helper có thể trông gọn hôm nay và trở thành hành lang hẹp mà mọi requirement sau này phải chen qua sự clever của người trước. Code đó không nhất thiết xấu. Nó chỉ đang phục vụ sai đối tượng.
Sự cầu toàn cũng có cách giấu nỗi sợ phía sau gu thẩm mỹ. Có lúc tôi polish không phải vì code thật sự cần thêm. Tôi polish vì mở PR nghĩa là làm công việc của mình visible. Reviewer có thể hỏi lại approach. Teammate có thể tìm ra đường đơn giản hơn. Một production issue có thể chứng minh rằng phần reasoning cẩn thận của tôi vẫn bỏ sót gì đó. Nếu cứ tinh chỉnh tiếp, tôi có thể trì hoãn khoảnh khắc bị phơi ra thêm một chút. Nhìn từ ngoài, perfection trông như kỷ luật; nhưng một phần của nó là né tránh.
Sự chuyển hướng lành mạnh hơn không phải là ngừng quan tâm. Nó là quan tâm tới những thứ khác. Maintainable code không phải code cẩu thả. Đó là code chừa chỗ cho người kế tiếp. Nó dùng tên gọi giải thích ý tưởng nghiệp vụ, không chỉ mẹo implementation. Nó chọn cấu trúc nhàm chán khi cấu trúc nhàm chán đã đủ. Nó giữ các tradeoff quan trọng ở nơi người đọc có thể thấy. Nó chấp nhận một chút repetition khi abstraction sẽ khó đổi hơn duplication. Nó ít quan tâm tới việc chứng minh tác giả biết nhiều đến đâu, và quan tâm nhiều hơn tới việc giảm lượng phỏng đoán mà người đọc sau phải mang theo.
Code dễ hiểu cũng tôn trọng quá trình review. Reviewer không nên phải giải một câu đố trước khi đánh giá rủi ro. Khi code được viết để có thể review, đường đi của change trở nên rõ hơn: cái gì đổi, cái gì giữ nguyên, behavior nào được test bảo vệ, và uncertainty còn nằm ở đâu. Điều đó không làm công việc kém sâu sắc hơn. Nó chỉ khiến phần sâu sắc ấy phục vụ một việc thật: giúp người khác có đủ confidence vào change.
Có một sự khiêm tốn rất lặng trong việc viết code dễ review. Nó thừa nhận rằng ý tưởng đầu tiên của tôi có thể chưa phải tốt nhất. Nó giả định team sẽ học thêm điều gì đó sau này. Nó để lại chỗ nối để future change có thể xảy ra mà không bắt mọi file trả giá cho lý thuyết hôm nay. Nó xem clarity như một dạng tử tế, không phải vì teammate không xử lý được complexity, mà vì ai cũng đã mang đủ complexity rồi.
Tôi mất thời gian để học điều này vì perfectionism cho phần thưởng cảm xúc rất nhanh. Một abstraction clever làm mình thấy đã ngay lập tức. Một function nhỏ, rõ ràng, hơi obvious đôi khi lại tạo cảm giác hụt hẫng, như thể mình không để lại dấu vết gì. Nhưng đoạn code giúp team đi tiếp thường ít kịch tính hơn đoạn code gây ấn tượng với chính tác giả. Nó không đòi được ngưỡng mộ. Nó chỉ âm thầm làm bug kế tiếp rẻ hơn, onboarding kế tiếp mượt hơn, và review kế tiếp ngắn hơn.
Tất nhiên, có một cách hiểu lười biếng của bài học này: good enough nghĩa là thế nào cũng được. Tôi không có ý đó. Có những đoạn code cần suy nghĩ rất sâu. Có những domain xứng đáng được model cẩn thận. Có những rủi ro nghiêm túc đến mức ta nên chậm lại, test nhiều hơn và challenge từng assumption. Vấn đề không phải là hạ thấp tiêu chuẩn. Vấn đề là hướng tiêu chuẩn vào đời sống của đoạn code sau khi nó rời tay mình.
Bây giờ, khi thấy mình đang đuổi theo code hoàn hảo, tôi thử hỏi một nhóm câu hỏi khác. Đoạn này thật sự dễ đọc hơn, hay chỉ làm tôi thấy thỏa mãn hơn? Sáu tháng nữa tôi có thể giải thích nó cho một teammate mới không? Reviewer có thấy được rủi ro mà không cần đoán intent của tôi không? Nếu requirement kế tiếp nghiêng sang hướng hơi khác, design này sẽ giúp hay sẽ cãi lại?
Tôi vẫn đổi tên biến. Tôi vẫn xóa những nhánh vụng. Tôi vẫn thích cảm giác một function rối trở nên rõ. Nhưng tôi đang học cách dừng sớm hơn, khi code đã đủ dễ hiểu để chia sẻ và đủ chắc để review. Công việc không chỉ trở thành thật sau khi nó hoàn hảo. Nó trở thành thật khi nó bước được vào cuộc trò chuyện của team.
Có lẽ đó là định nghĩa tốt hơn của code tốt: không phải code không có khuyết điểm, mà là code đủ rõ để khuyết điểm của nó có thể được tìm thấy, bàn bạc và cải thiện bởi nhiều hơn một người. Nếu bạn cũng từng có một giai đoạn đuổi theo code hoàn hảo, tôi sẽ rất vui được nghe điều gì đã giúp bạn đi về phía những đoạn code mà team thật sự có thể sống cùng.