Chapter 9: Variables and Readability

Trong chương này, bạn sẽ thấy sẽ cẩu thả sử dụng biến trong lập trình làm khó hiểu hơn. Cụ thể, có 3 vấn đề cần tranh luận:

  1. Khi có nhiều biến, sẽ khó hơn cho việc theo dõi tất cả.
  2. Một phạm vi biến lớn hơn, bạn phải theo dõi nó lâu hơn.
  3. Khi thường xuyên thay đổi biến, sẽ khó hơn để theo dõi giá trị hiện tại của nó.

Eliminating Variables: Loại bỏ các biến.

Trong chương 8, phần phá vỡ các biểu thức khổng lồ, chúng ta đã giới thiệu làm sao để “exlaining” hoặc “summary” biến có thể làm cho code dễ đọc. Các biến này hữu ích vì chúng phá vỡ các biểu thức khổng lồ và hoạt động như 1 dạng của tài liệu nữa :D

Trong phần này, chúng ta đang quan tâm đến loại bỏ các biến KHÔNG cải thiện khả năng đọc code. Khi 1 biến như vậy được xóa bỏ, code mới ngắn gọn hơn và thật dễ hiểu.

Trong phần dưới đây là 1 vài ví dụ tại sao các biến không cần thiết.

Useless Temporary Variables

Trong đoạn trích sau của Python code, xem xém biến now:

1
2
now = datetime.datetime.now()
root_message.last_view_time = now

Biến now có đáng để giữ? Ở đây có 3 lí do:

  • Nó không giúp phá vỡ các biểu thức phức tạp.
  • Nó không làm rõ thêm gì - biểu thức datetime.datetime.now() đã đủ rõ ràng.
  • Nó được sử dụng chỉ có 1 lần, bởi vậy nó không nén hoặc cắt giảm code.

Không sử dụng now, đoạn code lại dễ hiểu hơn:

1
root_message.last_view_time = datetime.datetime.now()

Các biến như now thường được gọi là “leftovers” (dịch tạm là đồ thừa) cái mà thừa thãi sau khi code đã được chỉnh sửa. Biến now nên được sử dụng trong nhiều nơi từ ban đầu. Hoặc có thể người code dự đoán sử dụng now nhiều lần nhưng không bao giờ thực sự cần nó.

Eliminating Intermediate Results: Loại bỏ các kết quả tạm trung gian

Đây là 1 ví dụ về hàm JavaScript loại bỏ 1 giá trị từ 1 mảng:

1
2
3
4
5
6
7
8
9
10
11
12
var remove_one = function (array, value_to_remove) {
var index_to_remove = null;
for (var i = 0; i < array.length; i += 1) {
if (array[i] === value_to_remove) {
index_to_remove = i;
break;
}
}
if (index_to_remove !== null) {
array.splice(index_to_remove, 1);
}
};

Biến index_to_remove chỉ được sử dụng để giữ kết quả trung gian. Những biến như vậy có thể đôi khi nên được loại bỏ bằng cách xử lý kết quả sớm nhất khi bạn có nó:
1
2
3
4
5
6
7
8
var remove_one = function (array, value_to_remove) {
for (var i = 0; i < array.length; i += 1) {
if (array[i] === value_to_remove) {
array.splice(i, 1);
return;
}
}
};

Bằng cách cho phép code trả về sớm, chúng ta không cần index_to_remove và đơn giản code hơn 1 chút.

Nói chung, nó là 1 chiến lực tốt để hoàn thành nhiệm vụ nhanh nhất có thể.

Eliminating Control Flow Variables

Đôi khi bạn nhìn thấy mẫu code trong vòng lặp:

1
2
3
4
5
6
7
8
9
boolean done = false;

while (/* condition */ && !done) {
...
if (...) {
done = true;
ontinue;
}
}

Biến done thậm chí có thể được set giá trị nhiều vị trí trong vòng lặp này.

Code như vậy thường đáp ứng 1 số nguyên tắc bất thành văn bạn cái mà bạn không nên thoát ra ở giữa vòng lặp. Nó không phải là 1 nguyên tắc như vậy.

Biến như done được gọi là “control flow variables“. Mục đích duy nhất của nó là chỉ đạo sự thực thi của chương trình - chúng không chứa 1 vài dữ liệu lập trình thực tế . Theo kinh nghiệm của chúng tôi, các biến kiểm soát luồng có thể thường được loại bỏ để tốt hơn sử dụng ở lập trình cấu trúc:

1
2
3
4
5
6
while (/* condition */) {
...
if (...) {
break;
}
}

Trường hợp này có vẻ dễ dàng để fix, nhưng điều gì sẽ xảy ra nếu nhiều vòng lặp lồng nhau trong mà không đơn giản để phá vỡ? Trong nhiều trường hợp phức tạp như vậy, giải pháp thường là moving chỗ code đó vào trong 1 hàm mới (trong hoặc ngoài vòng lặp tùy bạn)

DO YOU WANT YOUR COWORKERS TO FEEL LIKE THEY’RE IN AN INTERVIEW ALL THE TIME?

Eric Brechner của Microsoft đã nói chuyện về làm thế nào để có 1 câu hỏi phỏng vấn hay nên liên quan đến ít nhất 3 biến, p. 166.*). Nó có thể là vì giao dịch với 3 biến trong cùng 1 lúc buộc bạn phải suy nghĩ kĩ. Điều này có ý nghĩa trong 1 cuộc phỏng vấn, nơi mà bạn đang thử đẩy ứng viên đến các giới hạn. Nhưng bạn có muốn đồng nghiệp của bạn thấy chúng ta đang trong 1 cuộc phỏng vấn khi họ đang đọc code của bạn. :D

Shrink the Scope of Your Variables: Thu nhỏ phạm vi biến của bạn

Chúng ta đã nghe đến lời khuyên “tránh các biến toàn cục”. Đây là 1 lời khuyên tốt vì nó khó theo dõi chỗ nào và các biến toàn cục này đang được sử dụng như thế nào. Và “gây ô nhiễm không gian tên” (đặt 1 loạt các tên đó có thể mẫu thuẩn với các biến local của bạn), có thể vô tình sửa đổi các biến toàn cục khi nó dự định sử dụng 1 biến local hoặc phần khác xung quanh.

Trên thực tế, 1 ý tưởng tốt là “co phạm vi lại” cho tất cả các biến của bạn, không chỉ biến toàn cục.

KEY IDEA
Make your variable visible by as few lines of code as possible.

Rất nhiều ngôn ngữ lập trình phục vụ các scope/access levels bao gồm module, class, function và block scope. Sử dụng rất nhiều truy cập hạn chế là tốt hơn vì nó có nghĩa biến có thể “nhìn thấy” bởi ít dòng code hơn.

Tại sao lại cần làm vậy? Vì nó giảm đáng kể số lượng biến người đọc cần nghĩ trong cùng 1 lúc. Nếu bạn đang co tất cả các biến của bạn lại theo hệ số 2, sau đó trung bình sẽ có một nửa số biến trong phạm vi tại một thời điểm.

Ví dụ giả định bạn có 1 lớp rất lớn với 1 biến thành viên cái mà được sử dụng chỉ bởi 2 phương thức:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class LargeClass {
string str_;

void Method1() {
str_ = ...;
Method2();
}

void Method2() {
// Uses str_
}

// Lots of other methods that don't use str_ ...
};

Theo 1 số ý nghĩa, các biến thành viên cũng được coi là “mini-global” trong địa bàn của class. Đối với các lớp lớn hơn, rất khó để theo dõi tất cả các biến thành viên và phương thức nào định nghĩa lại mỗi chúng. Các mini-globale ít đi, điều đó sẽ tốt hơn.

Đối với trường hợp này nó có thể có hợp lý để “giáng cấp” str_to thành biến local:

1
2
3
4
5
6
7
8
9
10
11
12
class LargeClass {
void Method1() {
string str = ...;
Method2(str);
}

void Method2(string str) {
// Uses str
}

// Now other methods can't see str.
};

Một cách khác để hạn chế truy cập với các thành viên của class là tạo ra nhiều phương thức static nhất có thể. Các phương thức tĩnh là cách tốt để cho người đọc biết “những dòng code này bị cô lập với các biến đó”.

Một cách tiếp cận khác là phá vỡ các lớp lớn vào các lớp nhỏ hơn. Cách tiếp cận này hữu ích chỉ khi các lớp nhỏ hơn thực tế cô lập với nhau. Nếu bạn đang tạo 2 class mà truy cập mỗi thành phần của lớp khác, bạn chưa thực sự hoàn thành bất cứ điều gì.

Tương tự như vậy phá vỡ file lớn vào trong các file nhỏ, các hàm lớn thành các hàm nhỏ hơn. Một động lực lớn để làm như vậy là cô lập dữ liệu (ví dụ: các biến).

Nhưng ngôn ngữ khác nhau có những nguyên tắc khác nhau để định nghĩa chính xác các phạm vi. Chúng tôi muốn chỉ muốn chỉ ra 1 vài điểm thú vị, đáng quan tâm trong nguyên tắc phạm vi của biến =))

if Statement Scope in C++

Giả sử bạn có 1 đoạn mã C++ như sau:

1
2
3
4
5
6
PaymentInfo* info = database.ReadPaymentInfo();
if (info) {
cout << "User paid: " << info->amount() << endl;
}

// Many more lines of code below ...

Biến info sẽ còn lại trong phạm vi cho đoạn code còn lại của hàm, do đó mọi người đọc code sẽ giữ info trong đầu, tự hỏi nó sẽ sử dụng lại trong trường hợp nào, như thế nào?

Nhưng trong trường hợp này, info chỉ được sử dụng trong if. Trong C++ chúng ta có thể định nghĩa info với biểu thức điều kiện

1
2
3
if (PaymentInfo* info = database.ReadPaymentInfo()) {
cout << "User paid: " << info->amount() << endl;
}

Bây giờ người đọc đã có thể dễ dàng quên biến info ngoài phạm vi if này :D.

Creating “Private” Variables in JavaScript

Giả sử bạn có 1 biến cố chấp biến mà được sử dụng chỉ bởi 1 hàm:

1
2
3
4
5
6
7
8
9
submitted = false; // Note: global variable

var submit_form = function (form_name) {
if (submitted) {
return; // don't double-submit the form
}
...
submitted = true;
};

Biến toàn cục như submitted có thể gây ra cho người đọc đoạn code này rất nhiều sự sợ hãi. Dường như chỉ có hàm submit_form() sử dụng submitted nhưng bạn không biết chắc về điều đó. Trên thực tế, đoạn JavaScript khác cũng có thể sử dụng biến này với mục đích khác.

Bạn có thể chặn vấn đề này bằng cách bọ nó trong 1 closure

1
2
3
4
5
6
7
8
9
10
11
var submit_form = (function () {
var submitted = false; // Note: can only be accessed by the function below

return function (form_name) {
if (submitted) {
return; // don't double-submit the form
}
...
submitted = true;
};
}());

Nhận thấy rằng dấu ngoặc đơn ở dòng cuối là để thàm bên ngoài không tên được thực thi ngay lập tức, trả về hàm bên trong nó.

Nếu bạn chưa từng gặp kỹ thuật này trước đó, nó nhìn có vẻ lại trong lần đầu gặp. Nó có hiệu ứng tạo ra các phạm vi “private” cái mà chỉ cho các hàm trong nó truy cập. Bây giờ người đọc không phải tự hỏi Liệu submitted có được sử dụng ở đâu nữa không? và lo lắng về mâu thuẫn khi về các biến toàn cục khác cùng tên (Xem thêm về JavaScript The Good Part để hiểu thêm về kĩ thuật này).

JavaScript Global Scope

Trong JavaScript, nếu bạn bỏ sót từ khóa var từ khai báo biến (ví dụ x = 1 thay vì var x = 1) biến đó sẽ trở thành biến toàn cục, và tất cả chỗ trong file JavaScript, khối <script> đều truy cập được nó. Đây là 1 ví dụ:

1
2
3
4
5
6
7
8
<script>
var f = function () {
// DANGER: 'i' is not declared with 'var'!
for (i = 0; i < 10; i += 1) ...
};

f();
</script>

Đoạn code vô tình để i thành biến toàn cục, do đó khối sau vẫn nhìn thấy nó:

1
2
3
<script>
alert(i);
</script>

Rất nhiều lập trình viên không nhận thức được nguyên tắc về phạm vi này và nó gây ra các hành vi không mong muốn, có thể tạo ra các bug dị =)). Một biểu hiện chung của code này 2 hàm cùng có cả 2 biến local với cùng tên nhưng quên sử dụng var. Những hàm này sẽ vô tình “cross-talk” (nói chuyện chéo) và thương thay lập trình viên có thể kết luận rằng máy tính của anh ta đã chị chiếm hữu bởi ai đó hoặc RAM đã hỏng =)).

“Best practice” cho JavaScript là luôn luôn định nghĩa biến sử dụng từ khóa var (ví dụ var x = 1). Cách này sẽ giới hạn phạm vi của biến bên trong hàm nó được định nghĩa.

No Nested Scope in Python and JavaScript

Ngôn ngữ như C++ và Java có các khái niệm block scope - phạm vi khối, khi mà các biến được định nghĩa bên trong 1 if, for, try, hoặc các cấu trúc tương tự hạn chế phạm vi bên trong khối đó:

1
2
3
4
5
if (...) {
int x = 1;
}

x++; // Compile-error! 'x' is undefined.

Nhưng Python và JavaScript biến được định nghĩa trong 1 khối “tràn ra ngoài” cả hàm. Ví dụ, nhận thấy sử dụng example_value trong Python là hoàn toàn hợp lệ:

1
2
3
4
5
6
7
8
9
# No use of example_value up to this point.
if request:
for value in request.values:
if value > 0:
example_value = value
break

for logger in debug.loggers:
logger.log("Example:", example_value)

Nguyên tắc về scope này ngạc nhiên với nhiều lập trình viên và code như vậy rất khó để đọc. Trong 1 vài ngôn ngữ khác, dễ dàng tìm ra example_value trong lần định nghĩa đầu tiên - bạn có thể nhìn dọc “theo cạnh trái” của hàm bạn đang theo dõi.

Trong ví dụ trước cũng đang lỗi: nếu example_value không được set trong phần đầu của code, phần thứ 2 sẽ ném ra 1 ngoại lệ "NameError: 'example_value' is not defined". Chúng ta có thể sửa điều này, làm code có thể đọc được bằng cách định nghĩa example_value tại “tổ tiên chung gần nhất” (khái niệm của lồng nhau) nơi nó được sử dụng:

1
2
3
4
5
6
7
8
9
10
example_value = None

if request:
for value in request.values:
if value > 0:
example_value = value
break
if example_value:
for logger in debug.loggers:
logger.log("Example:", example_value)

Tuy nhiên, đây là trường hợp nơi example_value có thể loại bỏ hoàn toàn. example_value chỉ giữ kết quả trung gian và bạn có nhớ loại bỏ biến trung gian chúng ta đã thảo luận ở trang 95, biến như vậy có thể loại bỏ bằng cách “hoàn thành nhiệm vụ sớm nhất có thể”. Trong trường hợp này, nó có nghĩa log giá trị của ví dụ sớm nhất khi chúng ta tìm thấy nó.

Đây là đoạn code mới:

1
2
3
4
5
6
7
8
def LogExample(value):
for logger in debug.loggers:
logger.log("Example:", value)
if request:
for value in request.values:
if value > 0:
LogExample(value) # deal with 'value' immediately
break

Moving Definitions Down

Ngôn ngữ lập trình gốc C yêu cầu tất cả các biến được định nghĩa trên đầu của hàm và block. Yêu cầu này thật không may, vì đối với các hàm dài với nhiều biến, nó buộc người đọc nghĩ về tất cả các biến này, thậm chí chúng không được sử dụng (C và C++ đã loại bỏ yêu cầu này)

Đoạn code dưới đây, tất cả các biến mặc nhiên được định nghĩa trên đầu hàm:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def ViewFilteredReplies(original_id):
filtered_replies = []
root_message = Messages.objects.get(original_id)
all_replies = Messages.objects.select(root_id=original_id)

root_message.view_count += 1
root_message.last_view_time = datetime.datetime.now()
root_message.save()

for reply in all_replies:
if reply.spam_votes <= MAX_SPAM_VOTES:
filtered_replies.append(reply)

return filtered_replies

Vấn đề của ví dụ trên là nó buộc người đọc nghĩ về 1 biến trong 1 lúc, chuyển qua chuyển lại giữa chúng.

Vì người đọc không cần biết về tất cả chúng cho đến sau này. Sẽ dễ dàng hơn chỉ chuyển mỗi khai báo nào đúng trước khi nó được sử dụng trong lần đầu:

1
2
3
4
5
6
7
8
9
10
11
12
13
def ViewFilteredReplies(original_id):
root_message = Messages.objects.get(original_id)
root_message.view_count += 1
root_message.last_view_time = datetime.datetime.now()
root_message.save()

all_replies = Messages.objects.select(root_id=original_id)
filtered_replies = []
for reply in all_replies:
if reply.spam_votes <= MAX_SPAM_VOTES:
filtered_replies.append(reply)

return filtered_replies

Bạn có thể tự hỏi liệu biến all_replies là 1 biến cần thiết, hoặc nếu có thể xóa đi như sau:
1
2
for reply in Messages.objects.select(root_id=original_id):
...

Trong trường hợp này, all_replies đang là 1 biến explaining rất tốt, bởi vậy chúng tôi quyết định giữ nó.

Prefer Write-Once Variables

>

Write-Once Variables - Biến ghi 1 lần: tôi đang hiểu là gán giá trị 1 lần, và nghĩa rộng ra là gán giá trị cho nó ít nhất =))

Cho đến bây giờ trong chương này, chúng ta đã thảo luận là khó hơn để hiểu 1 chương trình với nhiều biến “trong cuộc chơi”. Thậm chí là còn khó hơn khi nghĩ về các biến thay đổi liên tục. Theo dõi giá trị của chúng làm tăng thêm 1 mức độ khó.

Để chống lại vấn đề này, chúng tôi gợi ý cho bạn 1 thứ nghe có chút lạ ưu tiên các biến ghi 1 lần

Biến “cố định vĩnh viễn” dễ dàng để nghĩ đến hơn. Chắc chắn, một biến hằng như:

1
static const int NUM_THREADS = 10;

Không yêu cầu người đọc phải nghĩ nhiều. Và với lý do giống nhau, sử dụng const trong C++ (và final trong Java) được khuyến khích cao nhất.

Trên thực tế, rất nhiều ngôn ngữ (bao gồm cả Python và Java), 1 vài kiểu build-in như string thì là bất biến. Theo như James Gosling (người tạo ra Java) nói “[Bất biến] có xu hướng gặp thường xuyên hơn hơn cả các vấn đề tự do” @@

Nhưng thậm chí nếu bạn không thể làm cho biến của bạn write-once, vẫn ưu ích nếu biến thay đổi ở ít chỗ hơn

KEY IDEA
Càng nhiều chỗ 1 biến được thao tác, càng khó lập luận, xem xét về giá trị hiện tại của nó

Vậy chúng ta làm như thế nào? Làm sao bạn thay đổi 1 biến trở thành write-once? Well, nó cần rất nhiều lần nó yêu cầu tái cấu trúc lại code 1 chút, bạn sẽ thấy trong ví dụ tiếp theo.

A Final Example

Trong ví dụ cuối cùng của chương này, chúng tôi muốn chỉ ra 1 ví dụ chứng minh nhiều nguyên lý chúng ta đã thảo luận.

Giả sử bạn có 1 trang web với số lượng input text fileds được sắp xếp như sau:

1
2
3
4
5
<input type="text" id="input1" value="Dustin">
<input type="text" id="input2" value="Trevor">
<input type="text" id="input3" value="">
<input type="text" id="input4" value="Melissa">
...

Như bạn đã thấy ids bắt đầu với input1 và tăng lên trong form.

Công việc của bạn là viết 1 hàm với tên setFirstEmptyInput() cái mà lấy 1 chuỗi và đặt nó vào <input> trống đầu tiên trên web (trong ví dụ trên là “input3”). Hàm này nên trả về DOM element đã được cập nhật (hoặc null nếu không còn đầu vào trống). Đây là 1 vài code cần làm, không áp dụng các nguyên tắc trong chương này:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var setFirstEmptyInput = function (new_value) {
var found = false;
var i = 1;
var elem = document.getElementById('input' + i);

while (elem !== null) {
if (elem.value === '') {
found = true;
break;
}
i++;
elem = document.getElementById('input' + i);
}


if (found) elem.value = new_value;
return elem;
};

Đoạn code hoạt động tốt nhưng chưa được đẹp. Điều gì đang có vấn đề và làm sao chúng ta có thể cải thiện chúng?

Có rất nhiều cách để nghĩ về cải thiện đoạn code này nhưng chúng ta sẽ xem xét từ quan điểm của các biến:

  • var found
  • var i
  • var elem

Tất cả các biến này đều tồn tại từ đầu vào của hàm, và được dùng lại nhiều lần. Hãy thử cải thiện mỗi biến này.

Như chúng ta đã thảo luận từ đầu của chương, các biến trung gian như found thường được loại bỏ bằng cách trả về sớm. Do đó đây là sự cả thiện:

1
2
3
4
5
6
7
8
9
10
11
12
13
var setFirstEmptyInput = function (new_value) {
var i = 1;
var elem = document.getElementById('input' + i);
while (elem !== null) {
if (elem.value === '') {
elem.value = new_value;
return elem;
}
i++;
elem = document.getElementById('input' + i);
}
return null;
};

Tiếp theo là biến elem. Nó được sử dụng nhiều lần trong code với vòng lặp và rất khó để giữ giá trị của nó. Đoạn code làm cho nó như nếu elem là 1 giá trị chúng ta đang lặp đi lặp lại, trong khi thực tế nó chỉ tăng giá trị của i. Do đó hãy tái cấu trúc lại đoạn code vòng lặp while vào trong vòng lặp for với i:
1
2
3
4
5
6
7
8
9
10
11
12
var setFirstEmptyInput = function (new_value) {
for (var i = 1; true; i++) {
var elem = document.getElementById('input' + i);
if (elem === null)
return null; // Search Failed. No empty input found.

if (elem.value === '') {
elem.value = new_value;
return elem;
}
};
}

Đặc biệt, chúng ta thấy cách elem hoạt động như 1 biến ghi 1 lần với vòng đời cố định trong vòng lặp. Sử dụng true trong 1 vòng lặp for không thường xuyên gặp, nhưng đổi lại chúng ta có thể xem được các khai báo và định nghĩa lại của i trong 1 dòng duy nhất. (Khi mà 1 while(true) cũng có thể hợp lý :D)

Túm lại

Trong chương này chúng ta nói về cách sử dụng biến trong lập trình có thể dồn lại nhanh chóng và trở nên khó theo dõi. Bạn có thể làm code của bạn dễ đọc hơn bằng cách đọc ít biến và làm cho chúng “lightweight” (nhẹ cân, kiểu tránh dùng nó làm nhiều thứ) nhất có thể. Cụ thể:

  • Loại bỏ các biến cản trở. Đặc biệt, chúng tôi đã chỉ ra 1 vài ví dụ làm sao để loại bỏ “các biến trung gian” bằng cách trả về ngay lập tức.
  • Giảm phạm vi của mỗi biến nhỏ nhất có thể. Di chuyển mỗi biến tới các vị trí nơi mà ít dòng nhất nhìn thấy nó. Không nhìn thấy là khỏi suy nghĩ =)) (Out of sight is out of mind.)
  • Ưu tiên các biến viết 1 lần. Các biến chỉ được set 1 lần (hoặc const, final hoặc các bất biến khác) làm code dễ hiểu hơn.
Author

Ming

Posted on

2020-05-24

Updated on

2024-08-08

Licensed under

Comments