Nghệ thuật viết code đẹp - Phần I: Viết flow điều kiện và vòng lặp dễ hiểu
Khi mới tiếp nhận một dự án đã được phát triển từ trước, hay nhận nhiệm vụ maintain một hệ thống đã chạy từ rất lâu rồi; chắc hẳn không ít lần bạn ngửa mặt lên trời chửi thề thằng viết ra những dòng code ấy kiểu như thế này: Thế không nào mà lắm for lồng nhau vậy? (đáng nhẽ chỉ cần thuật toán độ ...
Khi mới tiếp nhận một dự án đã được phát triển từ trước, hay nhận nhiệm vụ maintain một hệ thống đã chạy từ rất lâu rồi; chắc hẳn không ít lần bạn ngửa mặt lên trời chửi thề thằng viết ra những dòng code ấy kiểu như thế này:
- Thế không nào mà lắm for lồng nhau vậy? (đáng nhẽ chỉ cần thuật toán độ phức tạp O(1)) mà nó dùng tới O(n^2~3) thế này?)
- Sao lắm if else lồng nhau thế nhỉ?
- Sao đọc mãi một function mà không biết nó muốn return ra cái gì cả? @@
- Lúc gặp một bug nào đó thì không tài nào tìm được nổi dòng code nào gây ra lỗi ấy... ... vân vân và mây mây.
Và bạn nản chí, không muốn đọc tiếp và tìm hiểu về hệ thống đó nữa. Y như một chàng trai thấy cô gái xấu rồi nên chả thèm tìm hiểu xem tính cách cô ấy thế nào nữa. Có thể cô gái ấy có tâm hồn rất đẹp, hay nhà cô ấy rất giàu...; hệ thống kia có thể có Design rất tốt, tổ chức các class tuyệt vời... Nhưng ấn tượng để lại trong bạn chỉ là những dòng code xấu xí và khó hiểu mà thôi.
Hôm nay chúng ta sẽ cùng nhau tìm ra cách viết những dòng code sao cho người khác đọc được có thể hiểu nhanh nhất, muốn tìm hiểu hệ thống của chúng ta và muốn code được những dòng code "đẹp" như thế. Các phần trong series:
Nghệ thuật viết code đẹp - Phần II: Nên viết comment như thế nào? Nghệ thuật viết code đẹp - Phần III: Đơn giản, dễ đọc hoá biểu thức
Key!
Với những flow code về điều kiện và vòng lặp thì cố gắng viết một cách thật "tự nhiên". Tránh để người đọc phải ngừng đọc giữa chừng hay quay lại đọc những dòng code đã đọc ở trước.
Vậy cụ thể hoá của Key đó là gì?
1. Sắp xếp thứ tự của biến số trong biểu thức điều kiện
Các bạn hãy cùng xem các ví dụ dưới đây xem cách viết nào đọc "tự nhiên" hơn? VD1:
if (length >= 10)
và
if (10 <= length)
VD2:
while (bytes_received < bytes_expected)
và
while (bytes_expected > bytes_received)
Hầu hết các lập trình viên sẽ thấy code theo cách đầu tiên dễ đọc hơn. Nguyên tắc chính của nó là.
"Đặt đối tượng muốn so sánh (thay đổi) phía bên trái, và đối tượng dùng để so sánh phía bên phải (ít thay đổi)"
Trong thực tế một vài ngôn ngữ sẽ có lỗi khi lập trình viên sơ xuất viết
if (length = 10)
Vì viết như vậy không vi phạm syntax của ngôn ngữ nên rất khó debug. Có một số sách khuyên nên viết thành
if (10 = length)
để compiler báo lỗi khi biên dịch. Tuy nhiên các viết này không tự nhiên và sẽ gây khó chịu cho người đọc.
2. Sắp xếp thứ tự của điều kiện trong if/else
Chúng ta hãy cùng xem xét 2 cách viết dưới đây. Cách 1.
if (a != expectedValue) { // Các xử lý -> return null; } else { // Các xử lý -> return giá trị; }
Cách 2.
if (a == expectedValue) { // Các xử lý -> return giá trị; } else { // Các xử lý -> return null; }
Rõ ràng các bạn thấy đọc cách viết thứ 2 cảm thấy thoải mái hơn. Vì nó mang lại cảm giác tích cực vì đưa ra được trường hợp trả về giá trị mà lập trình viên mong muốn trước.
Để xử lý với if/else chúng ta có 3 nguyên tắc cơ bản sau
- Cố gắng đưa vế khẳng định lên trước thay vì để trường hợp phủ định lên
- Đưa trường hợp xử lý đơn giản và ra kết quả nhanh lên trước
- Đưa trường hợp quan trọng, có ý nghĩa hơn lên trước
3. Tránh vòng lặp do/while
Tại sao vòng lặp do/while lại không nên dùng? Vì bạn rất dễ gặp lỗi sai trong vòng do đầu tiên do chưa check điều kiện. Giống như bạn trượt từ đỉnh núi xuống nhưng gặp quá nhiều chướng ngại vật, bạn ngã lăn lộn xuống chân núi mới thấy có cái biển ghi là "Có nhiều chướng ngại vật nguy hiểm, cấm trượt". Đáng nhẽ cái biển đó phải đặt ở đỉnh núi, tức là trước vòng Do mới đúng =))
Đơn cử như một ví dụ này thôi:
do { continue; } while (false);
Theo các bạn thì vòng lặp này sẽ kéo dài vĩnh viễn hay sẽ chạy chỉ 1 lần? Cái này các bạn tự thử đi nhé! (yaoming)
4. Nhanh chóng return kết quả
Có 2 nguyên nhân để chúng ta viết một function trả về kết quả càng sớm càng tốt.
- Người đọc nắm được mục đích của function sớm hơn
- Giảm số lượng check trong trường hợp kết quả được return sớm -> tăng tốc độ chương trình
5. Không sử dụng goto
Quy tắc này có lẽ trong trường đại học các thầy cô cũng nhắc các bạn nhiều rồi. Sử dụng goto gây khó khăn rất lớn cho việc debug vì code thực thi của các bạn sẽ nhảy linh ta linh tinh đi khắp nơi; nhiều trường hợp sẽ rơi vào vòng lặp vĩnh viễn mà không biết đầu đuôi nó ở đâu...vv Trong hầu hết các trường hợp thì chúng ta có thể thay cách viết dùng goto bằng một cách viết khác. Vây nên các bạn hãy chịu khó bỏ thêm chút thời gian nhé
6. Chú ý khi sử dụng toán tử 3 ngôi
Mới đọc qua câu lệnh này liệu các bạn có biết nó trả ra giá trị nào?
return exponent >= 0 ? mantissa * (1 << exponent) : mantissa / (1 << -exponent);
Quy tắc ở đây là:
Việc viết code để người khác hiểu được thì quan trọng hơn là viết code sao cho ngắn nhất!
Chúng ta có thể viết lại như dưới đây:
if (exponent >= 0) { return mantissa * (1 << exponent); } else { return mantissa * (1 << -exponent); }
Toán tử 3 ngôi chỉ nên sử dụng khi điều kiện của nó đơn giản. Về cơ bản các bạn hãy sử dụng if/else nhé!
7. Hạn chế for và if lồng nhau.
Cái này chắc hiển nhiên rồi và cách thường dùng nhất là nhanh chóng trả về kết quả sớm nhất có thể để hạn chế đi sâu vào các vòng for và if bên trong.
Tới đây mình đã trình bày với các bạn phần 1 của series Nghệ thuật viết code đẹp. Có rất nhiều nguyên tắc cần chú ý tuỳ theo từng trường hợp. Nhưng tổng quan lại thì điều quan trọng nhất là mỗi khi các bạn đặt ngón tay lên bàn phím gõ ra những dòng code, thì hãy dành một chút thời gian nghĩ tới những người sẽ đọc hiểu và làm việc với chúng sau này. ^^