Ở các phần trước trong series, chúng ta đã tìm hiểu về những thứ cơ bản nhất khi viết code, đó là các vấn đề liên quan tới: khai báo và dùng biến, comment code, cũng như format code sao cho dễ dàng theo dõi. Phần tiếp theo đây, chúng ta sẽ đi sâu hơn một chút để phân tích về các cấu trúc điều khiển và biểu diễn logic. Trong bất kì ngôn ngữ lập trình nào, mặc dù có sự khác biệt về mặt cú pháp, nhưng tất cả đều có một điểm chung, đó là: cấu trúc điều khiển. Cụ thể, bất kì là ngôn ngữ nào, cấu trúc điều khiển luôn bao gồm 2 thứ quan trọng: xử lí rẽ nhánh và vòng lặp.
Với tiêu chí đã đề ra xuyên suốt series này, code cần phải dễ đọc và dễ hiểu, nhiệm vụ của chúng ta là đảm bảo các xử lí rẽ nhánh và thực hiện lặp sao cho việc xử lí đó “tự nhiên” nhất có thể. Nghĩa là, người đọc không cần phải đọc đi đọc lại code nhiều lần để có thể hiểu được nội dung code – lí tưởng nhất là họ chỉ cần đọc 1 lần.
Ý tưởng chính: Đảm bảo các xử lí rẽ nhánh và vòng lặp được thực hiện một cách “tự nhiên” nhất có thể.
Để làm được điều này, chúng ta hãy xem xét những vấn đề sau đây.
Thứ tự của các tham số trong câu điều kiện
Bạn hãy thử đọc 2 cách viết code sau đây, liệu bạn có sự khác biệt nào chăng?
// Cách 1
if ( length >= 10 ) {
// do something
// ...
}
// Cách 2
if (10 <= length) {
// do something
// ...
}
Có thể sự khác biệt là không nhiều, nhưng chắc hẳn là hầu hết chúng ta sẽ thấy cách viết đầu tiên thì dễ đọc và hiểu hơn cách thứ 2. Lí do rất đơn giản, cách 1 trông thuận mắt và có vẻ tự nhiên hơn.
Điều này giống như việc bạn được hỏi về khoảng cách từ nhà tới chỗ làm, giả sử quãng đường là tầm xấp xỉ tầm hơn 4km nhưng nhỏ hơn 5km. Lúc đó, bạn sẽ nói “Khoảng cách từ nhà tới văn phòng nhỏ hơn 5km” hay là “5km thì lớn hơn khoảng cách từ nhà tới văn phòng”? Hẳn rằng, bạn sẽ nói theo cách 1 phải không nào!
Vậy nên hãy chú ý: Khi tạo một lệnh so sánh, hãy đặt biến có khả năng thay đổi giá trị nằm bên trái, bên phải sẽ dành cho hằng số hoặc mốc so sánh.
Có 1 lưu ý cũng khá thú vị mà mình cũng muốn nhắc tới, đó là việc so sánh bằng nhau. Hãy thử xem xét đoạn code sau:
// Cách 1
if ( length == 0 ) {
// do something
// ...
}
// Cách 2
if (0 == length) {
//do something
}
Nếu xét như tiêu chí vừa nói ở trên, chúng ta sẽ chọn cách 1 để viết. Nhưng, cách 2 lại có một điểm khá hay, đó là cách 2 sẽ giảm thiểu khả năng bạn viết so sánh sai khi bạn viết thiếu dấu =
. Tức là, nếu viết 0 = length
thì chương trình sẽ báo lỗi, còn nếu viết length = 0
thì chương trình sẽ hiểu nhầm là bạn đang gán giá trị, lỗi logic này đôi khi gây hậu quả rất nghiêm trọng. Lỗi thiếu dấu =
khi so sánh cũng hay xảy ra, và nó tương đối khó bị phát hiện. Vậy nên hãy cẩn thận nhé.
Thứ tự của các xử lí if / else
Với các xử lí if/else nhiều trường hợp, lời khuyên mà những người có kinh nghiệm đưa ra là, chúng ta nên ưu tiên xử lí trước những tình huống sau:
- Ưu tiên các trường hợp đơn giản để tránh bỏ quên.
- Ưu tiên các trường hợp lạ/hiếm để tránh bị bỏ sót.
- Các trường hợp so sánh khẳng định thì dễ đọc hơn là so sánh phủ định.

Hình dung rằng bạn là nam, và đang muốn tuyển người yêu, điều kiện tiên quyết là ứng viên là nữ, không chấp nhận nam giới. Ngoài ra cần phải đẹp gái, hiền lành, dễ thương, biết lo lắng và quan tâm người yêu, không quá 30 nhưng lớn hơn 18, ..v.v.. Lúc đó bạn xử lí thế nào?
// Cách 1
if (person.gender == "Male") {
return false;
}
else if (person.age > 30 || person.age < 18) {
return false;
}
else if (person is cute, lovely, careful, ...) {
return "Hello! Cho anh lam quen nhe!";
}
// Cách 2
if (person.gender != "Male"
&& person.age > 18
&& person.age < 30
&& person is cute, lovely, careful, ...)
{
return "Hello! Cho anh lam quen nhe!";
}
else {
return false;
}
Bạn sẽ chọn cách 1 hay cách 2?
Với cách 2, chúng ta có một mệnh đề if
rất phức tạp, đương nhiên là nó khó hiểu rồi. Vừa có so sánh phủ định bắt chúng ta nghĩ ngược, lại nhiều điều kiện, điều này làm chúng ta khó theo dõi và bao quát vấn đề, code kiểu này dễ dẫn tới những lỗi tiềm năng.
Với cách 1, chúng ta tách điều kiện lớn thành những điều kiện con, xử lí từng trường hợp đơn giản trước, như vậy logic của chúng ta rõ ràng và tách bạch hơn rất nhiều. Ngoài ra, việc logic được rõ ràng cũng giúp chúng ta tránh được những lỗi không đáng có.
Tương tự, những trường hợp lạ hoặc hiếm xảy ra, chúng ta cũng nên ưu tiên xử lí trước để tránh bị bỏ sót:
function readFromFile( file ) {
if (file == null) {
return false;
}
// do something here
// ...
}
Tránh dùng nhiều xử lí lồng nhau
Khi viết code, chúng ta cũng nên tránh viết các xử lí rẽ nhánh lồng vào nhau quá nhiều. Lấy ví dụ về việc chọn bạn gái ở phần trên làm minh hoạ: giả sử ban đầu yêu cầu của bạn chỉ là “đối tượng là nữ”, đoạn code trông như sau:
if (person.gender != "Male") {
return "Hello! Cho anh lam quen nhe";
}
else {
return false;
}
Sau một thời gian, bạn có thêm một tiêu chí: độ tuổi từ 18 đến 30. Bạn sửa code như sau:
if (person.gender != "Male") {
if (person.age > 18 && person.age < 30) {
return "Hello! Cho anh lam quen nhe!";
}
return false;
}
else {
return false;
}
Sau nữa bạn tiếp tục lại phát sinh thêm tiêu chí: chiều cao phải lớn hơn 160cm. Lúc đó bạn lại sửa lại code như sau:
if (person.gender != "Male") {
if (person.age > 18 && person.age < 30) {
if (person.height > 160) {
return "Hello! Cho anh lam quen nhe!";
}
return false
}
return false;
}
else {
return false;
}
Với cách viết như trên, thật ra thì code vẫn chạy được, nhưng bây giờ nó trở thành một thứ gì đó trông có vẻ rất phức tạp và khó theo dõi. Lí do bởi vì có quá nhiều xử lí rẽ nhánh lồng vào nhau, khiến cho code trở nên rất khó đọc.
Để xử lí những tình huống này, ta sẽ vận dụng một mẹo nhỏ sau đây để giúp code tối ưu hơn: xử lí trường hợp đơn giản trước, return ngay khi có thể. Khi đó code của ta trông như sau:
if (person.gender == "Male") {
return false;
}
if (person.age < 18 || person.age > 30) {
return false;
}
if (person.height < 160) {
return false;
}
return "Hello! Cho anh lam quen nhe";
Mặc dù có dài hơn đôi chút, nhưng code mới của ta trông thuận mắt và dễ hiểu hơn nhiều rồi phải không nào? Và điều quan trọng nhất là code đã “phẳng” hơn trước. Kĩ thuật này đặc biệt có ích trong xử lí rẽ nhánh trong vòng lặp, code sẽ trông gọn gàng và đơn giản hơn nhiều.
Hạn chế việc sử dụng do…while
Có lẽ bạn không có gì xa lạ với vòng lặp do…while phải không nào. Về mặt logic, tất cả những loại vòng lặp trong ngôn ngữ lập trình đều có chung một ý nghĩa, đó là xử lí lặp đi lặp lại một đoạn code nào đó chừng nào điều kiện kiểm tra còn thoả mãn.
Một cách tự nhiên, trước khi làm bắt kể điều gì, điều đầu tiên chúng ta sẽ làm là kiểm tra tính hợp lệ của xử lí. Đó cũng là lí do mà bất kể xử lí rẽ nhánh (if/else, switch/case, …) hoặc vòng lặp (for, while, …) nào cũng đều cần được xử lí kiểm tra điều kiện trước tiên. Vòng lặp do…while hơi khác 1 chút: nó sẽ thực hiện xử lí đoạn code trước khi kiểm tra điều kiện. Hơi kì lạ! Thử đọc nhanh đoạn code sau đây:
do {
var_dump("Loop");
continue;
}
while (false);
Các bạn trả lời xem kế quả của đoạn code này là gì? Vòng lặp sẽ chạy bao nhiêu lần? Sau đó hãy thử trả lời cùng câu hỏi đó cho đoạn code sau đây:
while (false) {
var_dump("Loop");
continue;
}
Đoạn code này dễ trả lời hơn nhiều phải không nào? Có lẽ, bạn cũng cần phải thừa nhận một điều rằng: vòng lặp do…while có logic khó hiểu hơn và nó có vẻ “kém tự nhiên” hơn nhiều so với các vòng lặp khác. Điều may mắn là: những logic xử lí được bởi do…while cũng sẽ xử lí được bởi những vòng lặp khác. Vậy nên, đừng chọn cách viết khó hiểu làm gì.
Tóm lại
Trên đây là những cách mình đã thử áp dụng trong khi viết code, kết quả là code trông thuận mắt, tự nhiên và rõ ràng hơn rồi. Tóm lại, những điều chúng cần lưu ý khi sử dụng các cấu trúc điều khiển trong lập trình là:
- Khi so sánh, tham số thường đứng bên trái, hằng số hoặc cột mốc sẽ đứng bên phải.
- Xử lí cách trường hợp đơn giản hoặc hiếm khi xảy ra trong rẽ nhánh trước, cũng hạn chế dùng so sánh phủ định trong các xử lí logic.
- Hạn chế việc viết code lồng vào nhau bằng cách return sớm nếu có thể.
- Tránh dùng vòng lặp do…while, hãy dùng những vòng lặp khác.
Hãy nhớ tiêu chí ban đầu: Các xử lí điều khiển cần phải “tự nhiên”. Chỉ đơn giản thế thôi nhé.