Debug hiệu quả nghĩa là bạn thu hẹp phạm vi lỗi bằng quan sát có chủ đích. Bạn không nên đoán rồi thử ngẫu nhiên từng chỗ. Bên mình từng gặp nhiều bạn mới học mất cả buổi tối chỉ vì loay hoay sửa mò. Cách làm ấy trông có vẻ nhanh tay. Thực chất nó thường khiến bạn quay vòng quanh đúng một vấn đề, và cảm giác mệt mỏi cứ tích tụ sau mỗi lần chạy thử.
Sửa mò là gì và vì sao nó làm mất thời gian?
Sửa mò là khi bạn đổi vài dòng code, chạy lại và hy vọng lỗi tự biến mất. Cách này nhìn qua thì linh hoạt. Vấn đề là bạn thường không biết thay đổi nào thực sự tác động đến kết quả.
Giả sử lỗi biến mất sau khi bạn sửa. Bạn vẫn chưa chắc đó là nhờ cách sửa của mình hay do một yếu tố ngẫu nhiên khác. Vì thế bạn không rút ra được bài học nào cho lần sau.
Hậu quả hay gặp là sửa chỗ này làm hỏng chỗ kia. Có lúc lỗi cũ tưởng đã hết, rồi vài hôm sau nó quay lại dưới dạng khác và khó nhận ra hơn. Bên mình từng theo dõi một bạn sinh viên sửa cùng một lỗi ba lần trong một tuần. Lần nào bạn ấy cũng chỉ che được triệu chứng. Nguyên nhân gốc thì chưa bao giờ được tìm ra.
Người mới dễ rơi vào thói quen này vì ngại đọc kỹ thông báo lỗi. Họ cũng ngại dừng lại để suy nghĩ. Việc liên tục gõ phím và chạy thử tạo cảm giác đang tiến triển, trong khi thời gian thì trôi đi thật.
Tái hiện lỗi ổn định trước khi sửa
Trước khi sửa bất cứ thứ gì, bạn cần làm cho lỗi xuất hiện lại một cách đều đặn. Nếu lỗi lúc có lúc không, bạn không có cách nào kiểm chứng việc sửa có tác dụng thật hay không. Một lỗi chưa tái hiện được thì mọi thay đổi chỉ là phỏng đoán.
Hãy ghi lại đầu vào, môi trường chạy và các bước dẫn đến lỗi. Ví dụ bạn nhập số âm hay chuỗi rỗng. Bạn chạy trên Windows hay Linux. Bạn dùng phiên bản Java nào. Những chi tiết tưởng nhỏ này thường lại là manh mối quan trọng nhất.
Tiếp theo, hãy tìm đầu vào tối thiểu vẫn gây lỗi. Nếu một danh sách mười nghìn phần tử gây lỗi, thử lại với ba phần tử. Đầu vào càng nhỏ, bạn càng dễ thấy lỗi nằm ở đâu, vì có ít thứ hơn để loại trừ.
- Ghi lại đầu vào chính xác và môi trường chạy
- Rút gọn đầu vào xuống mức nhỏ nhất vẫn gây lỗi
- Chỉ bắt đầu sửa khi đã tái hiện được lỗi ít nhất hai lần liên tiếp
Quy trình khoanh vùng lỗi theo từng bước
Khi đã tái hiện được lỗi ổn định, việc tiếp theo là khoanh vùng. Hãy hình dung chương trình như một đường ống, nơi dữ liệu đi qua nhiều khâu liên tiếp. Câu hỏi cần trả lời là dữ liệu còn đúng đến khâu nào, và bắt đầu sai từ khâu nào.
Cách đơn giản nhất là chia đoạn chương trình thành các khối, rồi kiểm tra giá trị ở ranh giới giữa chúng. Ở mỗi ranh giới, bạn in hoặc xem giá trị biến. Nếu đến một điểm nào đó dữ liệu bắt đầu sai, lỗi nằm trong đoạn vừa đi qua. Cách này thủ công nhưng rất chắc chắn.
Phương pháp chia đôi cũng rất hữu ích. Bạn xác định nửa nào của đoạn code đang chứa lỗi, rồi lặp lại đúng thao tác đó trên nửa ấy. Mỗi lần chia đôi, phạm vi giảm đi một nửa. Chỉ vài bước là bạn đã tới đúng dòng cần tìm.
Quan trọng nhất là đặt điểm kiểm tra. Đó là một chỗ bạn dừng lại để xem giá trị có đúng với kỳ vọng hay không. Nhờ điểm kiểm tra, bạn biết chắc dữ liệu đúng đến đâu và không còn phải đoán.
Công cụ debug nên dùng và cách dùng đúng
Trình debugger tích hợp trong IDE là công cụ mạnh nhất, vậy mà nhiều bạn mới lại ít khi dùng tới. Bạn đặt breakpoint ở dòng nghi ngờ, chạy chương trình, rồi quan sát giá trị biến ngay tại thời điểm đó. Bạn cũng có thể đi từng dòng để thấy biến thay đổi thế nào qua từng bước.
Log có cấu trúc khác hẳn việc in linh tinh bằng System.out.println rải khắp nơi. Log có cấu trúc gắn kèm mức độ, thời điểm và ngữ cảnh rõ ràng. Khi đọc lại, bạn biết dòng nào thuộc luồng nào. Điều này đặc biệt cần thiết khi chương trình chạy nhiều luồng hoặc trên máy chủ thật.
Assertion và kiểm thử đơn vị giúp bắt lỗi sớm hơn nhiều. Một assertion khẳng định điều gì đó phải đúng tại một thời điểm cụ thể. Nếu sai, chương trình báo ngay, bạn không phải chờ lỗi lan sang chỗ khác mới phát hiện.
| Công cụ | Mục đích chính | Khi nào nên dùng |
|---|---|---|
| Debugger của IDE | Xem trạng thái biến và luồng chạy | Lỗi logic, cần đi từng bước |
| Log có cấu trúc | Theo dõi hành vi khi chương trình chạy thật | Lỗi chỉ xuất hiện trên môi trường thật hoặc khó tái hiện |
| Assertion | Chặn sớm khi giả định bị vi phạm | Kiểm tra điều kiện bắt buộc bên trong hàm |
| Kiểm thử đơn vị | Khoá hành vi của từng hàm | Sau khi sửa, để chắc lỗi không quay lại |
Với người mới, bên mình khuyên học debugger trước. Nó cho thấy trực tiếp chương trình đang nghĩ gì ở từng thời điểm. Log sẽ phát huy tác dụng khi chương trình chạy ở những nơi bạn không thể ngồi nhìn trực tiếp.
Đọc thông báo lỗi và stack trace đúng cách
Thông báo lỗi hay bị bỏ qua vì trông dài và khó hiểu. Thực ra nó chứa gần như toàn bộ manh mối bạn cần. Bạn chỉ cần biết đọc đúng thứ tự.
Dòng đầu cho biết loại lỗi, chẳng hạn NullPointerException hay IndexOutOfBoundsException. Trong stack trace, các dòng phía trên thường trỏ đến nơi lỗi phát sinh. Hãy tìm dòng đầu tiên thuộc về code của chính bạn. Đọc từ trên xuống rồi lần ngược theo các lệnh gọi sẽ cho thấy đường đi dẫn tới lỗi.
Lỗi cú pháp thường được trình biên dịch báo ngay. Lỗi lúc chạy xuất hiện khi chương trình đang thực thi. Còn lỗi logic thì chương trình vẫn chạy bình thường nhưng kết quả sai. Phân biệt được ba loại này giúp bạn chọn đúng công cụ để xử lý.
Khi gặp một thông báo lạ, hãy tra cứu có hệ thống. Tìm đúng nguyên văn thông báo, đọc tài liệu chính thức của thư viện, rồi mới đọc các bài giải thích khác. Đừng vội copy đoạn code đầu tiên tìm thấy, vì nó có thể đang giải quyết một tình huống khác hẳn.
Ví dụ thực tế: khoanh vùng một lỗi tính toán
Một bạn trong nhóm học của bên mình viết chương trình tính điểm trung bình cho báo cáo học tập. Kết quả cuối cùng luôn thấp hơn đáp án kỳ vọng khoảng vài phần trăm. Bạn ấy bắt đầu sửa công thức chia, rồi đổi kiểu dữ liệu, nhưng kết quả vẫn sai như cũ.
Bên mình hướng dẫn bạn dừng việc sửa lại và tái hiện lỗi với ba môn có điểm đơn giản. Sau đó bạn đặt điểm kiểm tra sau mỗi bước: đọc điểm, quy đổi hệ số, cộng dồn, rồi chia. Ở bước quy đổi hệ số, giá trị đã bị làm tròn sai ngay từ đầu.
Nguyên nhân là một biến kiểu số nguyên được gán từ một phép chia số thực, nên phần thập phân đã mất trước khi nhân với hệ số. Chỉ khi nhìn đúng chỗ đó, bạn ấy mới sửa một dòng và lỗi biến mất hoàn toàn.
Bài học rút ra là lỗi tính toán thường nằm ở chỗ chuyển đổi kiểu dữ liệu, chứ không hẳn ở công thức. Quan sát giá trị giữa các bước tiết kiệm thời gian hơn nhiều so với việc đọc đi đọc lại công thức.
Sai lầm thường gặp khi debug
Sai lầm đầu tiên là sửa triệu chứng thay vì nguyên nhân gốc. Ví dụ bạn thêm một câu kiểm tra null ngay chỗ lỗi xuất hiện, trong khi giá trị null thực ra đến từ một hàm phía trước không trả về đúng kết quả.
Sai lầm thứ hai là thay đổi nhiều chỗ cùng lúc. Khi đó, nếu lỗi biến mất, bạn không biết thay đổi nào thực sự có tác dụng. Bạn cũng có thể vô tình đưa thêm lỗi mới vào mà không hay.
Sai lầm thứ ba là không ghi lại những gì đã thử. Sau một tiếng, bạn quên mất mình đã thử cách nào và lại vô tình lặp lại đúng cách cũ. Một danh sách ngắn các bước đã làm sẽ giúp bạn thoát khỏi vòng lặp này.
Thói quen giúp debug nhanh hơn theo thời gian
Viết hàm nhỏ, mỗi hàm chỉ làm một việc rõ ràng, là thói quen có lợi nhất. Hàm nhỏ dễ kiểm tra đầu vào và đầu ra hơn, nên khi lỗi xảy ra, bạn khoanh vùng nhanh hơn rất nhiều. Đây cũng là điểm mà bài cách xử lý exception trong Java nhấn mạnh khi nói về việc bắt và xử lý lỗi có chủ đích.
Hãy ghi chú lại những lỗi bạn đã gặp và cách xử lý chúng. Một cuốn sổ tay cá nhân đơn giản sau vài tháng sẽ thành tài liệu rất quý, vì lỗi hôm nay thường quay lại dưới dạng gần giống.
Luyện debug trên mã của người khác cũng rất có ích. Khi đọc code không phải của mình, bạn buộc phải dựa vào quan sát thay vì trí nhớ, và kỹ năng đó chuyển sang code của chính bạn rất tốt. Nếu đang xây nền tảng từ đầu, bạn có thể đi theo lộ trình học lập trình cho người mới để có thứ tự rõ ràng khi luyện các kỹ năng này.
Câu hỏi thường gặp
Debug mất bao lâu là bình thường với người mới?
Người mới có thể mất vài giờ cho một lỗi mà người có kinh nghiệm xử lý trong vài phút. Điều đó hoàn toàn bình thường. Đáng quan tâm hơn là bạn có đang tiến bộ trong cách khoanh vùng hay không, chứ không chỉ nhìn vào tốc độ tuyệt đối.
Nên học debugger trước hay log trước?
Bên mình khuyên học debugger trước. Nó cho thấy trực tiếp giá trị biến và luồng chạy, nhờ đó bạn hiểu chương trình sâu hơn. Log nên học sau, khi cần theo dõi hệ thống đang chạy thật.
Làm sao biết lỗi nằm ở code của mình hay ở thư viện?
Hãy tái hiện lỗi bằng một đoạn code tối giản chỉ gọi thư viện đó. Nếu lỗi vẫn xảy ra, khả năng cao nằm ở thư viện hoặc cách dùng nó. Nếu không, lỗi nằm ở phần code xung quanh. Đọc kỹ tài liệu và kiểm tra lại phiên bản thư viện cũng rất cần thiết.
Có nên nhờ người khác xem khi bị kẹt lâu không?
Có, đừng ngại. Nếu đã thử các bước khoanh vùng mà vẫn không tiến thêm, hãy mang theo bằng chứng cụ thể: đầu vào tối thiểu, thông báo lỗi và những gì đã thử. Điều đó giúp người kia hỗ trợ nhanh hơn nhiều so với một câu hỏi chung chung.
Nếu chỉ nhớ một điều từ bài này, hãy nhớ: trước khi sửa, hãy tái hiện lỗi; khi đã có lỗi ổn định, hãy khoanh vùng bằng quan sát. Bắt đầu từ một bài tập nhỏ ngay hôm nay, bạn sẽ thấy việc tìm lỗi nhẹ nhàng hơn nhiều. Bạn cũng có thể tham khảo thêm cách tiếp cận bài toán từng bước để củng cố nền tảng tư duy.