Khi gỡ lỗi một chương trình, trong hầu hết các trường hợp, phần lớn thời gian của bạn sẽ được dành để cố gắng tìm ra lỗi thực sự ở đâu. Sau khi tìm thấy sự cố, các bước còn lại (khắc phục sự cố và xác thực rằng sự cố đã được khắc phục) thường không đáng kể.

Trong bài học này, chúng ta sẽ bắt đầu khám phá cách tìm lỗi.

1. Tìm kiếm vấn đề thông qua kiểm tra code

Giả sử bạn đã nhận thấy một vấn đề và bạn muốn theo dõi nguyên nhân của vấn đề cụ thể đó. Trong nhiều trường hợp (đặc biệt là trong các chương trình nhỏ hơn), chúng ta có thể nhanh chóng tìm hiểu về vấn đề.

Hãy xem xét đoạn chương trình sau:

/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: cafedevn@gmail.com
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

int main()
{
    getNames(); // ask user to enter a bunch of names
    sortNames(); // sort them in alphabetical order
    printNames(); // print the sorted list of names
 
    return 0;
}

Nếu bạn mong đợi chương trình này sẽ in các tên theo thứ tự bảng chữ cái, nhưng thay vào đó, nó đã in chúng theo thứ tự ngược lại, vấn đề có thể nằm ở hàm sortNames . Trong trường hợp bạn có thể thu hẹp vấn đề xuống một hàm cụ thể, bạn có thể phát hiện ra vấn đề chỉ bằng cách xem code.

Tuy nhiên, khi các chương trình trở nên phức tạp hơn, việc tìm kiếm các vấn đề bằng cách kiểm tra code cũng trở nên phức tạp hơn.

Đầu tiên, có rất nhiều mã để xem xét. Nhìn vào mỗi dòng code trong một chương trình dài hàng nghìn dòng có thể mất một thời gian rất dài (chưa kể nó vô cùng nhàm chán). Thứ hai, bản thân code có xu hướng phức tạp hơn, với nhiều nơi có thể xảy ra sự cố. Thứ ba, hành vi của code có thể không cung cấp cho bạn nhiều manh mối về việc mọi thứ sẽ sai ở đâu. Nếu bạn đã viết một chương trình để đưa ra các khuyến nghị về lỗi và nó thực sự không tạo ra gì cả, có lẽ bạn sẽ không có nhiều cơ hội để bắt đầu tìm kiếm vấn đề.

Cuối cùng, lỗi có thể được gây ra bằng cách đưa ra các giả định xấu. Hầu như không thể phát hiện ra một lỗi trực quan gây ra bởi một giả định xấu, bởi vì bạn có thể đưa ra giả định tồi tệ tương tự khi kiểm tra code và không nhận thấy lỗi. Vì vậy, nếu chúng ta có một vấn đề mà chúng ta không thể tìm thấy thông qua kiểm tra code, làm thế nào để chúng ta tìm thấy nó?

2. Tìm kiếm sự cố bằng cách chạy chương trình

May mắn thay, nếu chúng ta không thể tìm thấy sự cố thông qua kiểm tra code, có một cách khác chúng ta có thể thực hiện: chúng ta có thể xem hành vi của chương trình khi chương trình chạy và cố gắng chẩn đoán sự cố từ đó. Cách tiếp cận này có thể được khái quát như:

  1. Tìm hiểu làm thế nào để tái tạo vấn đề
  2. Chạy chương trình và thu thập thông tin để thu hẹp nơi xảy ra sự cố
  3. Lặp lại bước trước cho đến khi bạn tìm thấy vấn đề

Đối với phần còn lại của chương này, chúng ta sẽ thảo luận về các kỹ thuật để tạo thuận lợi cho phương pháp này.

3. Tái tạo vấn đề

Bước đầu tiên và quan trọng nhất trong việc tìm ra vấn đề là có thể tái tạo vấn đề . Lý do rất đơn giản: cực kỳ khó tìm ra vấn đề trừ khi bạn có thể quan sát nó xảy ra.

Quay lại với sự tương tự của máy làm đá của chúng ta – giả sử một ngày nào đó bạn của bạn nói với bạn rằng máy làm đá của bạn không hoạt động. Bạn đi để xem xét nó, và nó hoạt động tốt. Làm thế nào bạn sẽ chẩn đoán vấn đề? Nó sẽ rất khó khăn. Tuy nhiên, nếu bạn thực sự có thể thấy vấn đề của máy làm đá không hoạt động, thì bạn có thể bắt đầu chẩn đoán tại sao nó không hoạt động hiệu quả.

Nếu một sự cố phần mềm là trắng trợn (ví dụ: chương trình gặp sự cố ở cùng một nơi mỗi khi bạn chạy nó) thì việc tái tạo vấn đề có thể là chuyện nhỏ. Tuy nhiên, đôi khi tái tạo một vấn đề có thể khó khăn hơn rất nhiều. Sự cố chỉ có thể xảy ra trên một số máy tính nhất định hoặc trong một số trường hợp cụ thể (ví dụ: khi người dùng nhập một số đầu vào nhất định). Trong những trường hợp như vậy, việc tạo ra một tập hợp các bước tái tạo có thể hữu ích. Các bước sinh sản là một danh sách các bước rõ ràng và chính xác có thể được theo dõi để khiến vấn đề tái diễn với mức độ dự đoán cao. Mục tiêu là có thể khiến vấn đề tái diễn càng nhiều càng tốt, vì vậy chúng ta có thể chạy chương trình của mình nhiều lần và tìm kiếm manh mối để xác định nguyên nhân gây ra sự cố. Nếu vấn đề có thể được sao chép 100% thời gian, đó là lý tưởng, nhưng khả năng tái tạo dưới 100% có thể ổn. Một vấn đề chỉ xảy ra 50% thời gian có nghĩa là sẽ mất gấp đôi thời gian để chẩn đoán sự cố, vì một nửa thời gian chương trình sẽ không xuất hiện vấn đề và do đó không đóng góp bất kỳ thông tin chẩn đoán hữu ích nào.

4. Tìm hiểu về các vấn đề

Một khi chúng ta có thể tái tạo vấn đề một cách hợp lý, bước tiếp theo là tìm ra vấn đề ở đâu trong code. Dựa trên bản chất của vấn đề, điều này có thể dễ hoặc khó. Vì lợi ích của ví dụ, giả sử chúng ta không có nhiều ý tưởng về vấn đề thực sự là gì. Làm thế nào để chúng ta tìm thấy nó?

Một sự tương tự sẽ phục vụ chúng ta tốt ở đây. Hãy chơi một trò chơi hi-lo. Tôi sẽ yêu cầu bạn đoán một số trong khoảng từ 1 đến 10. Với mỗi lần đoán bạn thực hiện, tôi sẽ cho bạn biết liệu mỗi lần đoán quá cao, quá thấp hay đúng. Một ví dụ của trò chơi này có thể trông như thế này:

You: 5
Me: Too low
You: 8
Me: Too high
You: 6
Me: Too low
You: 7
Me: Correct

Trong trò chơi trên, bạn không cần phải đoán mọi số để tìm số tôi đang nghĩ đến. Thông qua quá trình đoán và xem xét thông tin bạn học được từ mỗi lần đoán, bạn có thể về nhà trong số đúng với chỉ một vài lần đoán (nếu bạn sử dụng một chiến lược tối ưu, bạn luôn có thể tìm thấy số tôi nghĩ đến trong 4 lần đoán hoặc ít hơn).

Chúng ta có thể sử dụng một quy trình tương tự để gỡ lỗi chương trình. Trong trường hợp xấu nhất, chúng ta có thể không biết lỗi ở đâu. Tuy nhiên, chúng ta biết rằng vấn đề phải nằm ở đâu đó trong đoạn code thực thi giữa lúc bắt đầu chương trình và điểm mà chương trình biểu hiện triệu chứng không chính xác đầu tiên mà chúng ta có thể quan sát được. Điều đó ít nhất loại trừ các phần của chương trình thực thi sau triệu chứng quan sát đầu tiên. Nhưng điều đó vẫn có khả năng để lại rất nhiều code. Để chẩn đoán vấn đề, chúng ta sẽ đưa ra một số phỏng đoán đã học về vấn đề đang ở đâu, với mục tiêu nhanh chóng giải quyết vấn đề.

Thông thường, bất cứ điều gì đã khiến chúng ta nhận thấy vấn đề sẽ cho chúng ta một dự đoán ban đầu gần với vấn đề thực sự. Ví dụ: nếu chương trình không ghi dữ liệu vào một tệp khi cần, thì vấn đề có thể nằm ở đâu đó trong code xử lý ghi vào file (duh!). Sau đó, chúng ta có thể sử dụng chiến lược giống như hi-lo để thử và cách ly vấn đề thực sự ở đâu.

Ví dụ:

  • Nếu tại một thời điểm nào đó trong chương trình của chúng ta, chúng ta có thể chứng minh rằng sự cố chưa xảy ra, thì điều này tương tự với việc nhận được kết quả hi-lo quá thấp – chúng ta biết vấn đề phải ở đâu đó trong chương trình. Ví dụ: nếu chương trình của chúng ta bị sập ở cùng một nơi mọi lúc và chúng ta có thể chứng minh rằng chương trình không bị sập tại một thời điểm cụ thể trong quá trình thực thi chương trình, thì sự cố phải nằm sau code.
  • Nếu tại một thời điểm nào đó trong chương trình của chúng ta, chúng ta có thể quan sát hành vi không chính xác liên quan đến vấn đề, thì điều này tương tự với việc nhận kết quả hi-lo quá cao và chúng ta biết vấn đề phải ở đâu đó sớm hơn trong chương trình. Ví dụ: giả sử một chương trình in giá trị của một số biến x . Bạn đang mong đợi nó in giá trị 2 , nhưng nó đã in 8 thay thế. Biến x phải có giá trị sai. Nếu tại một thời điểm nào đó trong quá trình thực thi chương trình của chúng ta, chúng ta có thể thấy biến x đã có giá trị 8 , thì chúng ta biết vấn đề phải xảy ra trước thời điểm đó.

Sự tương tự hi-lo không hoàn hảo – đôi khi chúng ta cũng có thể loại bỏ toàn bộ các phần của code của chúng ta khỏi sự cân nhắc mà không thu được bất kỳ thông tin nào về vấn đề thực tế là trước hay sau thời điểm đó.

Chúng ta sẽ đưa ra ví dụ về cả ba trường hợp này trong bài học tiếp theo.

Cuối cùng, với đủ dự đoán và một số kỹ thuật tốt, chúng ta có thể tìm ra dòng chính xác gây ra vấn đề! Nếu chúng ta đưa ra bất kỳ giả định xấu nào, điều này sẽ giúp chúng ta khám phá nơi. Khi bạn loại trừ mọi thứ khác, điều duy nhất còn lại phải gây ra sự cố. Sau đó, nó chỉ là một vấn đề hiểu tại sao.

Chiến lược đoán nào bạn muốn sử dụng tùy thuộc vào bạn – chiến lược tốt nhất phụ thuộc vào loại lỗi đó, vì vậy bạn có thể muốn thử nhiều cách tiếp cận khác nhau để thu hẹp vấn đề. Khi bạn có được kinh nghiệm trong việc gỡ lỗi, trực giác của bạn sẽ giúp hướng dẫn bạn.

Vậy làm thế nào để chúng ta làm ra những trò đoán Có nhiều cách để làm như vậy. Chúng ta sẽ bắt đầu với một số cách tiếp cận đơn giản trong chương tiếp theo, và sau đó chúng ta sẽ xây dựng những điều này và khám phá những cách khác trong các chương sau.