“Bạn chưa thật sự hiểu rõ 1 vấn đề trừ khi bạn có thể giải thích được nó cho bà của mình”

— Albert Einstein

Khi giải thích một ý tưởng phức tạp cho ai đó, rất dễ dàng gây ra sự nhầm lẫn cho họ với những chi tiết nhỏ nhặt. Có thể giải thích một ý tưởng “bằng Tiếng Anh” cho người hiểu biết ít hơn có thể hiểu là một kỹ năng vô cùng giá trị. Nó đòi hỏi bạn phải chắt lọc những nội dung quan trọng nhất của ý tưởng. Việc này không chỉ giúp người khác dễ hiểu hơn, mà còn giúp bạn suy nghĩ rõ ràng hơn về các ý tưởng.

Kỹ năng này cũng nên được sử dụng khi các bạn “trình bày” code. Tác giả cho rằng “source code” là phương pháp chính để giải thích những gì chương trình thực hiện. Vì vậy, code nên được viết “bằng Tiếng Anh”.

Chương 12 sẽ sử dụng các kỹ thuật đơn giản giúp cho code của bạn rõ ràng hơn:

  • Mô tả những gì code cần phải làm, bằng Tiếng Anh
  • Chú ý đến các từ khóa và cụm từ được sử dụng trong mô tả
  • Viết code phù hợp với mô tả đó

1. Mô tả logic rõ ràng (Describing Logic Clearly)

Ví dụ dưới đây là một đoạn code từ một trang web PHP, nó kiểm tra xem người dùng có quyền xem trang hay không, và nếu không thì lập tức thông báo người dùng không có quyền xem trang

$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 ...

Như đã nhắc đến ở “Phần 2: Đơn giản hóa vòng lặp và logic” thì đoạn code trên logic là không dễ hiểu. Logic có thể được đơn giản hóa đi. Hãy bắt đầu bằng việc mô tả logic “bằng Tiếng Anh

Có 2 cách để có quyền xem trang web trên:

  1. Bạn là admin
  2. Bạn sở hữu “current document” (nếu có)

Trái lại, bạn không có quyền xem trang web

Và đây là giải pháp thay thế từ mô tả trên:

if (is_admin_request()) {
  // authorized 
} elseif ($document && ($document['username'] == $_SESSION['username'])) {
  // authorized 
} else {
  return not_authorized(); 
} 
// continue rendering the page ...

Phiên bản mới này hơi khác thường vì có tới 2 phần code rỗng, nhưng code đã nhỏ hơn và logic đơn, giản dễ hiểu hơn (Code đã không còn sự phủ định)

2. Biết rõ các thư viện hỗ trợ gì (Knowing Your Libraries Helps)

Ví dụ một trang web có “tip box” để đưa ra những gợi ý có ích

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

Có rất nhiều tip

<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 truy cập trang web, một trong các thẻ div sẽ  được hiển thị ngẫu nhiên và các tip còn lại sẽ được ẩn đi. Khi click vào “Show me another tip!” thì sẽ hiển thị tip tiếp theo. Đây là code cài đặt chức năng đó bằng cách sử dụng thư viện jQuery

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();
};

Code này OK nhưng có thể được viết tốt hơn. Thử mô tả bằng lời những gì code phải làm

  1. Tìm tip đang hiển thị hiện tại và ẩn nó đi
  2. Sau đó tìm tip tiếp theo và hiển thị
  3. Nếu đã đến tip cuối cùng thì quay lại tip đầu tiên

Dựa vào đó ta có thể viết lại code như sau

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
};

Giải pháp mới này chứa ít dòng code hơn và không phải thao tác trực tiếp với các số nguyên. Nó là cách mà một người sẽ nghĩ về code. Trong trường hợp này, jQuery có method .next() mà ta có thể sử dụng. Một cách để viết code ngắn gọn hơn là biết được những gì mà thư viện đã cung cấp.

3. Áp dụng phương pháp này cho các vấn đề lớn (Applying This Method to Larger Problems)

Các ví dụ trước ta đã áp dụng kỹ thuật cho các khối code nhỏ, trong ví dụ tiếp theo ta sẽ áp dụng vào một chức năng lớn hơn. Phương pháp này có thể giúp bạn xác định nhưng phần có thể tách nhỏ ra trong một chức năng lớn.

Tưởng tượng chúng ta có một hệ thống ghi lại lượt mua cổ phiếu. Mỗi giao dịch có 4 dữ liệu chính:

  1. time (chính xác ngày giờ mua)
  2. ticker_symbol (VD: GOOG)
  3. price (VD: $600)
  4. number_of_shares (VD: 100)

Dữ liệu được chia thành 3 bảng dữ liệu riêng biệt như ảnh sau. Time là khóa chính duy nhất.

Bây giờ ta cần viết chương trình để join 3 bảng với nhau, các hàng đều được sắp xếp theo thời gian, nhưng 1 số hàng bị thiếu. Ta cần tìm tất cả các hàng ở cả 3 bảng phù hợp về thời gian, và bỏ qua bất kỳ hàng nào không phù hợp (như hình).

Đây là đoạn code Python thực hiện tìm tất cả các hàng phù hợp

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()

Làm thế nào để đoạn code này dễ đọc hơn?

Một lần nữa, hãy mô tả “bằng Tiếng Anh” những gì chúng ta đang làm

  1. Ta lặp lại việc đọc 3 hàng từ 3 bảng song song (We are reading three row iterators in parallel.)
  2. Khi time của các hàng không giống nhau, tiếp tục đọc các hàng để chúng giống nhau (Whenever the rows’ times don’t line up, advance the rows so they do line up.)
  3. In ra các hàng phù hợp và tiếp tục đọc các hàng (Then print the aligned rows, and advance the rows again.)
  4. Lặp lại cho đến khi không còn hàng phù hợp (Keep doing this until there are no more matching rows left.)

Nhìn lại code thì phần rối nhất tương ứng với phần “advance the rows so they do line up”. Để “trình bày” code rõ ràng hơn, ta có thể tách phần logic lộn xộn ấy vào 1 function riêng AdvanceToMatchingTime().

Đây là phiên bản mới của code:

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()

Bạn có thể thấy rằng đoạn code đã dễ hiểu hơn, chúng ta đã ẩn đi phần chi tiết về sắp xếp các hàng.

Áp dụng phương pháp một cách đệ quy

AdvanceToMatchingTime() sẽ được viết y như phiên bản đầu

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

Và ta lại mô tả function này cần làm gì

  1. So sánh thời gian của mỗi hàng hiện tại, chúng giống nhau, xong
  2. Nếu không, một hàng bất kỳ sẽ tiếp tục đọc
  3. Lặp lại cho đến khi time các hàng giống nhau (hoặc một trong 3 vòng lặp kết thúc)

Mô tả đã rõ ràng hơn rất nhiều so với code trước đó. Và chú ý là mô tả không đề cập cụ thể đến các chi tiết các hàng như stock_iter … Ta có thể đổi tên biến đơn giản hơn và tổng quát hơn. Như sau:

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

Bạn có thể thấy, đoạn code rõ ràng hơn trước rất nhiều. Thuật toán trở nên đơn giản hơn, ít phép so sánh hơn và các biến tổng quát hơn.

Tổng kết

Chương này giải thích 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 các mô tả đó để viết code tự nhiên hơn. Kỹ thuật này rất đơn giản nhưng rất hiệu quả. Nhìn vào các mô tả bằng lời cũng có thể giúp bạn nhìn ra các vấn đề nhỏ. Ngoài ra, nếu bạn không thể mô tả vấn đề hay thiết kế của bạn bằng lời, có thể có gì đó đã bị thiếu hoặc chưa được xác định. Mô tả một chương trình, ý tưởng bằng lời chính là đã định hình cho chương trình, ý tưởng đó.