Chapter 12: Turning Thoughts into Code

You do not really understand something unless you can explain it to your grandmother.
—Albert Einstein

Khi giải thích 1 ý tưởng phức tạp cho ai đó, nó dễ dàng gây mơ hồ đối với họ với 1 vài chi tiết. Nó là 1 kĩ năng có giá trị để có thể giải thích 1 ý tưởng “bằng tiếng Anh” để 1 ai đó ít sự hiểu biết hơn bạn có thể hiểu.
Nó yêu cầu đặt 1 ý tưởng xuống với nhiều khái niệm quan trọng. Làm điều này không chỉ giúp người khác hiểu mà còn giúp bạn nghĩ về ý tưởng của mình rõ ràng hơn.

Kĩ năng giống như vậy nên được sử dụng khi “giới thiệu” code của bạn cho người đọc. Chúng ta xem mã nguồn và nó là cách cơ bản giải thích những gì 1 chương trình đan làm. Bởi vậy, code nên viết “in plain English” (bản rõ tiếng Anh)

Trong chương này, chúng tôi sẽ sử dụng quá trình đơn giản giúp bạn code rõ ràng hơn:

  1. Mô tả những thứ code cần làm, bằng tiếng Anh, như bạn muốn nói với đồng nghiệp
  2. Chú ý đến các từ khóa và cụm từ được sử dụng trong mô tả
  3. Viết code của bạn trùng với mô tả này.

Describing Logic Clearly

Đây là 1 đoạn mã từ 1 trang web trong PHP. Đoạn code này ở đầu một trang bảo mật. Nó kiểm tra chỗ nào người dùng xác thực để xem được page và nếu không, lập tức trả về 1 trang lỗi quyền:

1
2
3
4
5
6
7
8
9
10
11
$is_admin = is_admin_request();
if ($document) {
if (!$is_admin && ($document['username'] != $_SESSION['username'])) {
return not_authorized();
}
} else {
if (!$is_admin) {
return not_authorized();
}
}
// continue rendering the page ...

Chỉ có 1 chút logic trong code. Như bạn đã thấy từ phần II, Simplying Loops and Logic, cây logic lớn như này không dễ để hiểu. Những logic trong đoạn code này có thể được đơn giản hóa, nhưng bằng cách nào. Hãy bắt đầu mô tả nó bằng tiếng Anh:

1
2
3
4
There are two ways you can be authorized:
1) you are an admin
2) you own the current document (if there is one)
Otherwise, you are not authorized.

Đây là cách giải quyết thay thế theo cảm hứng mô tả này:

1
2
3
4
5
6
7
8
9
if (is_admin_request()) {
// authorized
} elseif ($document && ($document['username'] == $_SESSION['username'])) {
// authorized
} else {
return not_authorized();
}

// continue rendering the page ...

Phiên bản này hơi bất thường vì nó có 2 body trống. Nhưng đoạn code này nhỏ hơn và logic đơn giản hơn bởi vì nó không có phủ định (còn cách giải quyết trước có 3 “nots”). Dòng ở dưới dễ hiểu hơn.

Knowing Your Libraries Helps

Chúng ta đã từng có 1 trang web bao gồm “tips box” chỉ ra những gợi ý hữu ích cho người dùng như:

Tip: Log in to see your past queries. [Show me another tip!]

Có vài chục lời khuyên và tất cả chúng được ẩn trong HTML:

1
2
3
<div id="tip-1" class="tip">Tip: Log in to see your past queries.</div>
<div id="tip-2" class="tip">Tip: Click on a picture to see it close up.</div>
...

Khi người dùng thăm trang web, 1 trong những divs này ngẫu nhiên có thể thấy và phần còn lại vẫn ẩn.

Nếu click vào lin “Show me another tip!”, nó sẽ chuyển tới tip tiếp theo. Đây là đoạn code thự thi chức năng này bằng JQuery:

1
2
3
4
5
6
7
8
9
10
11
12
var show_next_tip = function () {
var num_tips = $('.tip').size();
var shown_tip = $('.tip:visible');

var shown_tip_num = Number(shown_tip.attr('id').slice(4));
if (shown_tip_num === num_tips) {
$('#tip-1').show();
} else {
$('#tip-' + (shown_tip_num + 1)).show();
}
shown_tip.hide();
};

Đoạn code này okay. Nhưng nó có thể tốt hơn. Hãy bắt đầu việc mô tả, bằng từ với những gì đoạn code này cố gắng làm:
1
2
3
Find the currently visible tip and hide it.
Then find the next tip after it and show that.
If we've run out of tips, cycle back to the first tip.

Dựa vào mô tả này, đây là cách khác giải quyết vấn đề:

1
2
3
4
5
6
7
8
var show_next_tip = function () {
var cur_tip = $('.tip:visible').hide(); // find the currently visible tip and hide it
var next_tip = cur_tip.next('.tip'); // find the next tip after it
if (next_tip.size() === 0) { // if we've run out of tips,
next_tip = $('.tip:first'); // cycle back to the first tip
}
next_tip.show(); // show the new tip
};

Cách giải quyết này ít code hơn và không phải thao tác số nguyên trực tiếp. Nó cũng liên kết nhiều hơn với cách mà 1 người sẽ nghĩ về đoạn code này

Trong trường hợp này, nó giúp jQuery có 1 phương thức .next() chúng ta có thể sử dụng. Một phần của việc viết mã ngắn gọn là nhờ thư viện

Applying This Method to Larger Problems

Ví dụ trước có đã áp dụng cho quá trình xử lý các khối code nhỏ. Trong ví dụ tiếp theo, chúng tôi sẽ áp dụng nó với các hàm lớn hơn. Như bạn sẽ thây, cách thức này có thể giúp bạn phá vỡ code bằng cách giúp bạn nhận định các phần code bạn có thể phá vỡ.

Tưởng tượng chúng ta có 1 hệ thống ghi lại việc mua bán cổ phiếu. Mỗi phiên có 1 phần sau:
• time (a precise date and time of the purchase)
• ticker_symbol (e.g., GOOG)
• price (e.g., $600)
• number_of_shares (e.g., 100)

Vì 1 ví do nào đó, dữ liệu được trải rộng ra 1 bảng cơ sở dữ liệu, như minh họa. Với mỗi database, time là khóa chính duy nhất:

Bây giờ, chúng tôi cần viết 1 hương trình join 3 bảng này lại với nhau (Sử dụng toán tử JOIN trong SQL). Bước này đơn giản vì các hàng đã được sắp xếp theo time, nhưng không may, một vài cột đã bị mất. Bạn muốn tìm tất cả các hàng với 1 times trùng nhau và bỏ qua 1 vài hàng không được xép, như bạn có thể thấy ở phần dưới

Đây là đoạn code Python tìm kiếm tất cả các hàng trùng nhau:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def PrintStockTransactions():
stock_iter = db_read("SELECT time, ticker_symbol FROM ...")
price_iter = ...
num_shares_iter = ...

# Iterate through all the rows of the 3 tables in parallel.
while stock_iter and price_iter and num_shares_iter:
stock_time = stock_iter.time
price_time = price_iter.time
num_shares_time = num_shares_iter.time

# If all 3 rows don't have the same time, skip over the oldest row
# Note: the "<=" below can't just be "<" in case there are 2 tied-oldest.
if stock_time != price_time or stock_time != num_shares_time:
if stock_time <= price_time and stock_time <= num_shares_time:
stock_iter.NextRow()
elif price_time <= stock_time and price_time <= num_shares_time:
price_iter.NextRow()
elif num_shares_time <= stock_time and num_shares_time <= price_time:
num_shares_iter.NextRow()
else:
assert False # impossible
continue
assert stock_time == price_time == num_shares_time
# Print the aligned rows.
print "@", stock_time,
print stock_iter.ticker_symbol,
print price_iter.price,
print num_shares_iter.number_of_shares

stock_iter.NextRow()
price_iter.NextRow()
num_shares_iter.NextRow()

Đoạn code này hoạt động, nhưng có quá nhiều thứ đang diễn ra với vòng lặp bỏ qua các hàng không trùng. Một số cờ cảnh báo có thể đã tóm tắt trong đầu bạn: Liệu có thể bỏ mất 1 số hàng? Nó có thể đọc đến cuối của vòng lặp không?

Vậy chúng ta có thể làm cho nó dễ đọc hơn?

An English Description of the Solution

Một lần nữa, hãy lùi lại và mô tả chúng bằng tiếng Anh những thử chúng ta thử làm:

We are reading three row iterators in parallel.

Whenever the rows’ times don’t line up, advance the rows so they do line up.

Then print the aligned rows, and advance the rows again.

Keep doing this until there are no more matching rows left.

Nhìn lại code ban đầu, phần lộn xộn nhất trong giao dịch khối “advance the rows so they do line up.”. Để trình bày đoạn mã rõ ràng hơn, chúng ta trích xuất tất cả các logic lộn xộn vào trong 1 hàm mới với tên là AdvanceToMatchingTime().

Và đây là phiên bản code mới, sử dụng hàm này:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def PrintStockTransactions():
stock_iter = ...
price_iter = ...
num_shares_iter = ...

while True:
time = AdvanceToMatchingTime(stock_iter, price_iter, num_shares_iter)
if time is None:
return

# Print the aligned rows.
print "@", time,
print stock_iter.ticker_symbol,
print price_iter.price,
print num_shares_iter.number_of_shares

stock_iter.NextRow()
price_iter.NextRow()
num_shares_iter.NextRow()

Như bạn có thể nhìn thấy, đoạn code này dễ hiểu hơn, chúng ta đã ẩn tất cả những code chi tiết bẩn thỉu của việc xếp các hàng đi

Applying the Method Recursively

Dễ dàng để tưởng tượng bạn sẽ viết AdvanceToMatchingTime() như thế nào - trong trường hợp tệ nhất, nó sẽ nhìn tương tự như khối code trong phiên bản đầu tiên:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def AdvanceToMatchingTime(stock_iter, price_iter, num_shares_iter):
# Iterate through all the rows of the 3 tables in parallel.
while stock_iter and price_iter and num_shares_iter:
stock_time = stock_iter.time
price_time = price_iter.time
num_shares_time = num_shares_iter.time

# If all 3 rows don't have the same time, skip over the oldest row
if stock_time != price_time or stock_time != num_shares_time:
if stock_time <= price_time and stock_time <= num_shares_time:
stock_iter.NextRow()
elif price_time <= stock_time and price_time <= num_shares_time:
price_iter.NextRow()
elif num_shares_time <= stock_time and num_shares_time <= price_time:
num_shares_iter.NextRow()
else:
assert False # impossible
continue

assert stock_time == price_time == num_shares_time
return stock_time

Nhưng hãy cải thiện đoạn code này bằng cách cách làm của chúng ta với AdvanceToMatchingTime(). Đây là mô tả những thử hàm này cần làm:
1
2
3
Look at the times of each current row: if they're aligned, we're done.
Otherwise, advance any rows that are "behind."
Keep doing this until the rows are aligned (or one of the iterators has ended).

Mô tả này rõ ràng và thanh lịch hơn đoạn code trước. Một thứ nhận ra là mô tả này không bao giờ nhắc đến stock_iter hoặc các chi tiết khác cụ thể cho vấn đề của chúng tôi. Điều đó có nghĩa chúng tôi có thể đổi tên biến thành đơn giản và tổng quát hơn. Đây là kết quả code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def AdvanceToMatchingTime(row_iter1, row_iter2, row_iter3):
while row_iter1 and row_iter2 and row_iter3:
t1 = row_iter1.time
t2 = row_iter2.time
t3 = row_iter3.time
if t1 == t2 == t3:
return t1

tmax = max(t1, t2, t3)
# If any row is "behind," advance it.
# Eventually, this while loop will align them all.
if t1 < tmax: row_iter1.NextRow()
if t2 < tmax: row_iter2.NextRow()
if t3 < tmax: row_iter3.NextRow()
return None # no alignment could be found

Như bạn có thể thấy, đoạn code này rõ ràng hơn trước rất nhiều. Thuật toán trở nên đơn giản hơn và bât giờ có ít hơn các phép so sánh, Và chúng ta đã sử dụng tên ngắn hơn như t1 và không phảo nghĩ về các cột cụ thể database liên quan.

Summary

Chương này thảo luận về các kĩ thuật đơn giản để mô tả chương trình của bạn bằng tiếng Anh và sử dụng mô tả này giúp bạn viết code 1 cách tự nhiên. Kĩ thuật này đơn giản nhưng rất công hiệu. Nhìn vào các từ và cụm từ được sử dụng trong mô tả có thể giúp bạn tìm ra các vấn đề con và ngắn chúng ra.

Nhưng quá trình “saying things in plain English” đơn giản là áp dụng bên ngoài việc viết code. Ví dụ, 1 chính sách của phòng thí nghiệm máy tính, khi sinh viên cần gỡ lỗi chương trình của anh ấy, đầu tiên anh ấy phải giải thích vấn đề đến 1 con gấu bông chuyên dụng ở góc phòng. Thật ngạc nhiên, chỉ mô tả vấn đề phát ra thành tiếng có thể giúp sinh viên tìm ra giải pháp. Kỹ thuật này được gọi là “rubber ducking” - vịt cao su.

Một cách khác để nhìn việc này: nếu bạn không thể mô tả vấn đề hoặc các ý định của bạn bằng từ, 1 vài thứ có thể quên hoặc chưa xác định. Bắt 1 chương trình (hoặc 1 vài ý tưởng) thành từ ngữ có thể thực sự bắt buộc nó thành hình (shape).

Author

Ming

Posted on

2020-05-24

Updated on

2021-04-10

Licensed under

Comments