Theo Dõi Từng Bước Tại Điểm Dừng Là Một Phương Pháp Gỡ Lỗi Không Hiệu Quả.
Phương pháp theo dõi từng bước bằng điểm ngắt là phương pháp gỡ lỗi cực kỳ kém hiệu quả
Bộ gỡ lỗi tương tác cho phép theo dõi từng bước từng bước với điểm ngắt là một phát minh mang tính bước ngoặt trong lịch sử phát triển phần mềm. Tuy nhiên tôi cho rằng, giống như giao diện đồ họa người-máy, đây là phương pháp đánh đổi hiệu suất để hạ thấp ngưỡng tiếp cận. Về bản chất, đây là phương pháp debug vô cùng phiền phức và tốn kém thời gian.
Trong giai đoạn niên thiếu (trước năm 2005 và hơn mười năm kinh nghiệm phát triển phần mềm), tôi cực kỳ phụ thuộc vào các bộ debug dạng này. Từ Turbo C đến Visual C++, tôi đã từng trải nghiệm tất cả các phiên bản một cách tỉ mỉ. Khi sử dụng bất kỳ công cụ nào đủ mười năm trời, việc thành thạo là điều tự nhiên. Tôi từng tự tin rằng mình đã tinh thông đủ để định vị chính xác các lỗi thông qua phương pháp debug kiểu này. Tuy nhiên từ năm 2005 trở đi khi chuyển sang phát triển đa nền tảng, có lẽ do chưa tìm được công cụ đồ họa phù hợp trên Linux, tôi đã có dịp ngẫm nghĩ lại về phương pháp debug. GDB tuy mạnh mẽ, nhưng giao diện đồ họa thời đó chưa hoàn thiện như bây giờ. Các công cụ phổ biến như Insight hay DDD đều gặp phải vài trục trặc nhỏ, trải nghiệm sử dụng không thực sự mượt mà. Tôi dần thay đổi cách làm việc. Ngoài việc nâng cao chất lượng mã nguồn: viết code gọn gàng, ít lỗi, tôi còn tích cực kiểm tra lại mã nguồn (Code Review), cố ý thêm nhiều log để định vị lỗi.
Khi trọng tâm công việc dần chuyển từ phát triển client đồ họa sang backend server, nhược điểm của việc dừng chương trình bằng debugger ngày càng thể hiện rõ ràng. Với phần mềm kiến trúc client-server, khi một bên bị dừng lại để debug từng bước theo nhịp điệu con người, thì bên kia vẫn vận hành theo nhịp độ máy tính. Giữ cho quy trình chạy phần mềm diễn ra bình thường là điều gần như bất khả thi.
Trong những năm làm việc tiếp theo, tôi dần tham gia vào một số dự án phát triển trên nền tảng Windows. Sau thêm mười năm rèn luyện nữa, dù偶尔 dùng lại các bộ debug tương tác, tôi cũng không còn thấy ưu thế của chúng. Thường thì tay vẫn máy móc nhấn các nút debug, nhưng đầu óc đã không còn tập trung vào dòng code đang hiển thị. Rất nhiều lần, chưa kịp chạy đến vị trí xuất hiện bug, tôi đã chợt nhận ra lỗi nằm ở đoạn code nào đó. Những trải nghiệm lặp đi lặp lại này khiến tôi dần nghi ngờ phương pháp debug truyền thống, tự hỏi điều gì đang khiến debugger trở nên kém hiệu quả.
Khi trò chuyện với đồng nghiệp về cách định vị bug, tôi thường nửa đùa nửa thật: “Cậu cứ mở trình soạn thảo ra, chăm chú nhìn vào code là được. Nhìn mãi nhìn mãi thì bug sẽ tự động sáng lóa lên thôi!” Dù là đùa vui nhưng theo quan điểm của tôi, không phương pháp debug nào sánh được với Code Review. Dù là code mình viết hay code của người khác, nhiệm vụ quan trọng nhất luôn là hiểu rõ cấu trúc tổng thể của chương trình.
Một chương trình máy tính luôn được cấu thành từ những đoạn mã tuần tự đan xen cấu trúc rẽ nhánh. Các đoạn code tuần tự cực kỳ ổn định - đầu vào tại điểm bắt đầu sẽ quyết định đầu ra cuối cùng. Chúng ta quan tâm nhiều đến trạng thái đầu vào, thường có thể bỏ qua quá trình xử lý trung gian, chỉ cần xem xét kết quả đầu ra. Bởi bất kể đoạn code tuần tự có dài đến đâu, nó cũng chỉ có duy nhất một lộ trình thực thi. Ngược lại, sự tồn tại của các cấu trúc rẽ nhánh khiến lộ trình xử lý dữ liệu thay đổi tùy theo trạng thái trung gian. Khi đánh giá tính đúng đắn của code, tất cả các điểm rẽ nhánh đều cần được xem xét kỹ lưỡng. Điều kiện nào khiến chương trình đi theo nhánh này? Điều kiện nào dẫn đến nhánh kia? Có thể nói số lượng rẽ nhánh trực tiếp quyết định độ phức tạp của code. Đây chính là tư tưởng nền tảng của chỉ số độ phức tạp McCabe đang được sử dụng rộng rãi hiện nay.
Độ phức tạp McCabe của một phần mềm hoàn chỉnh luôn vượt xa khả năng xử lý tức thời của bộ não con người. Tuy nhiên thông thường chúng ta có thể chia nhỏ hệ thống thành các module, cấu trúc module cao cấp với tính kết dính cao và mức độ liên kết yếu sẽ giúp giảm độ phức tạp tổng thể. Một module có tính kết dính cao có thể được cô lập với phần còn lại của hệ thống, giúp ta tập trung phân tích nội tại module đó. Khi phạm vi code cần xem xét đủ nhỏ, bộ não có thể xử lý toàn bộ lộ trình thực thi bao gồm mọi nhánh rẽ trong cùng một thời điểm. Trái ngược với việc dùng debugger để quan sát lộ trình thực thi chương trình - vốn chỉ diễn ra duy nhất một lộ trình ứng với một dữ liệu đầu vào cụ thể - khi phân tích bằng đầu óc thì hoàn toàn khác. Khi độ phức tạp McCabe không quá lớn, chúng ta gần như có thể xử lý song song toàn bộ lộ trình thực thi. Nói cách khác, trong lúc quét code, bạn vừa phân tích đồng thời tất cả các tình huống có thể xảy ra, vừa có thể lược bỏ các nhánh ít quan trọng. Tất nhiên, giống như bất kỳ kỹ năng nào khác, tốc độ phân tích, phạm vi xử lý (độ phức tạp) và độ chính xác trong việc lược bỏ các nhánh cần thời gian rèn luyện dài lâu. Việc phụ thuộc quá mức vào công cụ debug tương tác sẽ cản trở quá trình rèn luyện này - đầu óc sẽ bị cuốn vào các trạng thái tức thì: hiện tại chương trình đang chạy đến đâu, nên đặt điểm ngắt tiếp theo ở đâu, giá trị hiện tại của các biến ra sao… mà không còn dành thời gian suy nghĩ: nếu đầu vào là tình huống khác thì chương trình sẽ xử lý thế nào? Bởi công cụ đã tự động loại bỏ những quy trình chưa xảy ra, chờ bạn thiết kế một bộ đầu vào mới để hiện thực hóa chúng vào lần chạy sau.
Công cụ debug tương tác thường thiếu khả năng hồi quy (trace back) - chúng thường chỉ phản ánh trạng thái hiện tại mà không lưu trữ lịch sử. Một số hạn chế này có thể khắc phục qua cải tiến công cụ, nhưng nhiều hạn chế khác thì không. Một tình huống thường gặp: bạn đặt điểm ngắt tiếp theo, khi chương trình dừng lại bạn phát hiện trạng thái bất thường, chỉ xác định được lỗi nằm giữa hai lần dừng trước - sau, nhưng muốn tìm nguyên nhân xảy ra sự cố, hoặc biết chính xác trạng thái trung gian đã thế nào thì công cụ lại bó tay. Trái lại, khi dùng đầu óc để mô phỏng lộ trình thực thi chương trình, mọi thứ đều là bản đồ tĩnh - việc hồi quy và tiến hành đều có chung bản chất, chỉ khác ở việc tập trung vào vị trí nào trên trục thời gian. Đây chính là lý do tại sao những lập trình viên được rèn luyện bài bản có thể nhìn code đã biết bug nằm đâu, trong khi các chuyên gia debugger lại phải chạy đi chạy lại vài lần mới phát hiện ra lỗi.
Việc mô phỏng chính xác lộ trình thực thi chương trình trong đầu dĩ nhiên đòi hỏi quá trình rèn luyện nghiêm túc