Chapter 11: One Task at a Time

Code làm rất nhiều thứ 1 lúc rất khó hiểu. Một khối đơn của code có thể khởi tạo đối tượng mới, làm sạch dữ liệu, phân tích inputs và áp dụng logic, tất cả cùng 1 lúc. Nếu tất cả code đan xem với nhau, nó sẽ khó để hiểu hơn là mỗi “task” được bắt đầu và tự hoàn thành.

K E Y I D E A
Code should be organized so that it’s doing only one task at a time.

Nói cách khác, chương này đề cập đến vấn đề “phân mảnh” code của bạn. Theo dõi biểu đồ bên dưới để minh họa quá trình này: biểu đồ bên trái chỉ ra các đoạn mã khác nhau mà 1 phần của code đang làm và đoạn mã bên phải chỉ ra các đoạn code giống nhau sau khi nó được tổ chức làm 1 task 1 lúc.

Bạn chắc hẳn đã nghe lời khuyên “hàm chỉ nên làm 1 thứ”. Lời khuyên của chúng tôi tương tự vậy, nhưng nó không luôn luôn nói về ranh giới của hàm. Chắc chắn, phá vỡ các hàm lớn vào nhiều hàm nhỏ có thể tốt. Nhưng thậm chí nếu bạn không làm vậy, bạn có thể tổ chức code trong các hàm lớn để nó cảm thấy có các logic riêng biệt cho từng phần.

Đây là quá trình chúng ta sử dụng để tạo “one task at a time”:

  1. Liệt kê ra tất cả các “task” bạn của bạn đang làm. Chúng ta sử dụng từ “task” rất lỏng lẻo - nó có thể nhỏ nhất có thể như “đảm bảo object này là hợp lệ” hoặc mơ hồ như “thực hiện vòng lặp qua mỗi node của cây”.
  2. Thử tách các task này nhiều nhất bạn có thể đưa vào trong các hàm khác nhau hoặc ít nhất là trong các phần mã khác nhau.

Trong chương này, chúng tôi sẽ chỉ cho bạn 1 số ví dụ để làm việc này.

Tasks Can Be Small

Giả định bạn đang có 1 phần voting cho 1 blog, người dùng có thể “Up” hoặc “Down”. Các điểm sẽ được tính là mỗi vote “Up” thì +1 và mỗi “Down” thì -1. Có 1 trạng thái người dùng có thể vote như hình sau ảnh hưởng đến điểm của bạn.

Khi người dùng click 1 button bạn thay đổi điểm bằng đoạn JavaScript sau:

1
vote_changed(old_vote, new_vote); // each vote is "Up", "Down", or ""

Hàm cập nhật tổng điểm số và làm việc với cặp old_vote/new_vote như sau:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var vote_changed = function (old_vote, new_vote) {
var score = get_score();

if (new_vote !== old_vote) {
if (new_vote === 'Up') {
score += (old_vote === 'Down' ? 2 : 1);
} else if (new_vote === 'Down') {
score -= (old_vote === 'Up' ? 2 : 1);
} else if (new_vote === '') {
score += (old_vote === 'Up' ? -1 : 1);
}
}

set_score(score);
};

Mặc dù code nhìn khá ngắn nhưng nó đang làm nhiều việc. Có rất nhiều chi tiết phức tạp và khó để nói xem lướt qua có bất kì lỗi, chính tả hoặc các lỗi khác không?

Đoạn code này có vẻ như chỉ cần làm 1 việc (cập nhật điểm số) nhưng thực tế nó đang làm 2 thứ cùng 1 lúc:

  1. old_votenew_vote đang được “parsed” vào giá trị số
  2. score đang được cập nhật

Chúng ta có thể dễ dàng đọc hơn bằng cách giải quyết mỗi việc trên riêng lẻ. Đoạn code dưới đây giải quyết nhiệm vụ đầu tiên, chuyển giá trị vote thành số:

1
2
3
4
5
6
7
8
9
10
var vote_value = function (vote) {
if (vote === 'Up') {
return +1;
}
if (vote === 'Down') {
return -1;
}

return 0;
};

Bây giờ phần còn lại của code là xử nhiệm vụ thứ 2, cập nhật điểm:
1
2
3
4
5
6
7
var vote_changed = function (old_vote, new_vote) {
var score = get_score();
score -= vote_value(old_vote); // remove the old vote
score += vote_value(new_vote); // add the new vote

set_score(score);
};

Như bạn có thể thấy, phiên bản code này tốn ít nỗ lực về mặt tinh thần hơn để tự nhủ rằng nó hoạt động

As you can see, this version of the code takes a lot less mental effort to convince yourself that it works. =))

Đây là 1 phần lớn làm code “dễ dàng để hiểu”.

Extracting Values from an Object

Chúng tôi đã từng có 1 số định dạng JavaScript để định dạng 1 vị trí của user thành 1 chuỗi thân thiện “City, Country như “Santa Monica, USA” or “Paris, France.”. Chúng ta nhận vào 1 từ điển location_info với nhiều cấu trúc dữ liệu. Tất cả cái chúng ta chọn là “City” và “Country” từ tất cả các trường và nối chúng lại với nhau.

Hình sau minh hoạt ví dụ input/output:

Có vẻ nó đơn giản cho đến lúc này, nhưng phần bẫy ở đây là một hoặc tất cả 4 giá trị này có thể thiếu. Đây là cách chúng ta trao đổi với điều này:

  • Khi chọn “City” chúng ta ưu tiên chọn “LocalityName” (city/town) nếu có sẵn, sau đó là “SubAdministrativeAreaName” (larger city/county), sau đó là “AdministrativeAreaName” (state/territory).
  • Nếu cả 3 giá trị đều thiếu, “City” sẽ nhận giá trị đầu vào là “Middle-of-Nowhere”
  • Nếu “CountryName” thiếu, “Planet Earth” sẽ được sử dụng làm mặc định.

Đây là đoạn code chúng ta viết để thực hiện task trên:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var place = location_info["LocalityName"]; // e.g. "Santa Monica"
if (!place) {
place = location_info["SubAdministrativeAreaName"]; // e.g. "Los Angeles"
}
if (!place) {
place = location_info["AdministrativeAreaName"]; // e.g. "California"
}
if (!place) {
place = "Middle-of-Nowhere";
}
if (location_info["CountryName"]) {
place += ", " + location_info["CountryName"]; // e.g. "USA"
} else {
place += ", Planet Earth";
}

return place;

Chắc chắn nó hơi lộn xộn, nhưng nó hoạt động tốt.

Nhưng 1 vài ngày sau, chúng ta cần cải thiện vấn đề này: đối với vị trí từ United States, chúng tôi muốn hiển thị state thay vì conntry (hoàn toàn khả thi). Do đó thay vì “Santa Monica, USA” nó sẽ trả về “Santa Monica, California”.

Để thêm tính năng này vào phần code trước sẽ làm cho nó xấu đi rất nhiều.

Applying “One Task at a Time”

Thay vì uốn đoạn mã này theo ý chúng ta, chúng ta hãy dừng lại và nhận ra nó đang thực hiện quá nhiều task trong cùng 1 lúc:

  1. Trích xuất giá trị từ từ điển location_info
  2. Duyệt qua 1 thứ tự trên cho “City” và mặc định là “Middle-of-Nowhere” nếu nó không tìm thấy gì
  3. Nhận “Country” và sử dụng “Planet Earth” nếu không có giá trị
  4. Cập nhật place

Thay vì đó, chúng ta viết lại đoạn code ban đầu để giải quyết mỗi vấn đề độc lập.

Nhiệm vụ đầu tiên (trích xuất giá trí từ từ điển) dễ dàng giải quyết:

1
2
3
4
var town    = location_info["LocalityName"];                // e.g. "Santa Monica"
var city = location_info["SubAdministrativeAreaName"]; // e.g. "Los Angeles"
var state = location_info["AdministrativeAreaName"]; // e.g. "CA"
var country = location_info["CountryName"]; // e.g. "USA"

Đến điểm này, chúng tôi đã sử dụng location_info và không phải nhớ nhiều và các keys không trực quan. Thay vào đó, chúng ta đã có những biến đơn giản để làm việc cùng.

Tiếp theo, chúng tôi đã tìm ra cái “nửa thứ 2” của giá trị trả về sẽ như:

1
2
3
4
5
6
7
8
// Start with the default, and keep overwriting with the most specific value.
var second_half = "Planet Earth";
if (country) {
second_half = country;
}
if (state && country === "USA") {
second_half = state;
}

Tương tự, chúng tôi tìm “first half”:
1
2
3
4
5
6
7
8
9
10
var first_half = "Middle-of-Nowhere";
if (state && country !== "USA") {
first_half = state;
}
if (city) {
first_half = city;
}
if (town) {
first_half = town;
}

Cuối cùng chúng ta nối các thông tin lại với nhau:
1
return first_half + ", " + second_half;

Hình minh họa của chương trình chống phân mảnh ở đầu chương này thực sự là một đại diện cho giải pháp ban đầu và phiên bản mới này. Đây là minh họa tương tự, với nhiều chi tiết được điền vào:

Như bạn có thể thấy, 4 tasks trong giải pháp thứ 2 đã được phân mảnh vào khu vực riêng biệt.

Another Approach

Khi tái cấu trúc code, có nhiều cách để làm nó và trường hợp này cũng không phải là ngoại lệ. Một khi bạn đã tách 1 vài tasks, code sẽ dễ hiểu hơn và bạn cần tiếp tục tìm cách tốt hơn để tái cấu trúc nó.

Ví dụ, dễ dàng hơn khi loạt câu lệnh if yêu cầu 1 vài sự cận trọng đọc để viết nếu mỗi trường hợp hoạt động có chính xác hay không? Thực tế có 2 nhiệm vụ con đang diễn ra đồng thời trong đoạn code này:

  1. Duyệt qua danh sách các biến và chọn cái được ưu tiên nhất tồn tại.
  2. Sử dụng 1 danh sách khác, tùy thuộc vào quốc gia là “USA”

Nhìn lại, bạn có thể thấy code trước đó có logic “if USA” đan xem với phần logic còn lại. Thay vào đó, chúng ta có thể xử lý USA và non-USA riêng biệt:

1
2
3
4
5
6
7
8
9
10
11
var first_half, second_half;

if (country === "USA") {
first_half = town || city || "Middle-of-Nowhere";
second_half = state || "USA";
} else {
first_half = town || city || state || "Middle-of-Nowhere";
second_half = country || "Planet Earth";
}

return first_half + ", " + second_half;

A Larger Example

Chúng tôi xây dựng 1 hệ thống web-crawling, 1 hàm với tên UpdateCounts() được gọi và tăng các thống kê khác nhau sau mỗi trang web được download:

1
2
3
4
5
void UpdateCounts(HttpDownload hd) {
counts["Exit State" ][hd.exit_state()]++; // e.g. "SUCCESS" or "FAILURE"
counts["Http Response"][hd.http_response()]++; // e.g. "404 NOT FOUND"
counts["Content-Type" ][hd.content_type()]++; // e.g. "text/html"
}

Well, đây là cách mà chúng tôi mong muốn nhìn thấy của code!

Đặc biệt đối tượng HttpDownload không có phương thức nào hiển thị ở đây. Thay vào đó, HttpDownload là 1 class rất lớn và phức tạp, với nhiều class lồng nhau và chúng ta phải tự tìm ra các giá trị đó. Để làm cho mọi thứ tệ hơn, đôi khi các giá trị này bị thiếu hoàn toàn - trong trường hợp này chúng ta chỉ sử dụng giá trị mặc định “unknown”.

Vì tất cả code với nhau, code thực sự có vẻ hơi hỗn độn:

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
34
35
// WARNING: DO NOT STARE DIRECTLY AT THIS CODE FOR EXTENDED PERIODS OF TIME.
void UpdateCounts(HttpDownload hd) {
// Figure out the Exit State, if available.
if (!hd.has_event_log() || !hd.event_log().has_exit_state()) {
counts["Exit State"]["unknown"]++;
} else {
string state_str = ExitStateTypeName(hd.event_log().exit_state());
counts["Exit State"][state_str]++;
}

// If there are no HTTP headers at all, use "unknown" for the remaining elements.
if (!hd.has_http_headers()) {
counts["Http Response"]["unknown"]++;
counts["Content-Type"]["unknown"]++;
return;
}

HttpHeaders headers = hd.http_headers();

// Log the HTTP response, if known, otherwise log "unknown"
if (!headers.has_response_code()) {
counts["Http Response"]["unknown"]++;
} else {
string code = StringPrintf("%d", headers.response_code());
counts["Http Response"][code]++;
}

// Log the Content-Type if known, otherwise log "unknown"
if (!headers.has_content_type()) {
counts["Content-Type"]["unknown"]++;
} else {
string content_type = ContentTypeMime(headers.content_type());
counts["Content-Type"][content_type]++;
}
}

Như bạn có thể thấy, rất nhiều code và rất nhiều logic và thậm chí 1 vài dòng code đã lặp lại. Không hề vui vẻ gì khi đọc đoạn code này.

Đặc biệt, đoạn code này có thể chuyển qua chuyện lại giữa các nhiệm vụ khác nhau. Ở đây có các nhiệm vụ khác nhau xen kẽ trong code:

  1. Sử dụng “unknown” làm giá trị mặc định cho mỗi key.
  2. Phát hiện xem những thành phần nào của HttpDownload đang thiếu.
  3. Trích xuất giá trị và chuyển nó thành string.
  4. Cập nhật counts[].

Chúng ta có thể cải thiện đoạn code bằng cách phân tách 1 vài tasks này ra thành các phần riêng trong code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void UpdateCounts(HttpDownload hd) {
// Task: define default values for each of the values we want to extract
string exit_state = "unknown";
string http_response = "unknown";
string content_type = "unknown";

// Task: try to extract each value from HttpDownload, one by one
if (hd.has_event_log() && hd.event_log().has_exit_state()) {
exit_state = ExitStateTypeName(hd.event_log().exit_state());
}
if (hd.has_http_headers() && hd.http_headers().has_response_code()) {
http_response = StringPrintf("%d", hd.http_headers().response_code());
}
if (hd.has_http_headers() && hd.http_headers().has_content_type()) {
content_type = ContentTypeMime(hd.http_headers().content_type());
}

// Task: update counts[]
counts["Exit State"][exit_state]++;
counts["Http Response"][http_response]++;
counts["Content-Type"][content_type]++;
}

Như bạn có thể thấy, đoạn code này gồm 3 khu vực với các mục đích sau:

  1. Định nghĩa giá trị mặc định cho 3 keys chúng ta quan tâm.
  2. Trích xuất giá trị, nếu tồn tại, với mỗi keys này, chuyển chúng thành chuỗi.
  3. Cập nhật counts cho mỗi key/value.

Điều tốt nhất tách chúng ra thành các khu vực riêng biệt là chúng cô lập với các phần khác - trong khi bạn đang đọc 1 khu vực, bạn không nghĩ về khu vực khác.

Bạn có thể nhận thấy rằng chúng ta đã liệt kê ra 1 tasks, nhưng chúng ta có thể chỉ tách biệt chỉ 3 trong số chúng. Điều này hoàn toàn ổn: các tasks mà bạn liệt kê ban đầu chỉ là các điểm khởi đầu. Thậm chí tách một số trong số chúng có thể giúp mọi thứ rất nhiều, như nó đã làm ở đây.

Further Improvements

Phiên bản code mới đã cải thiện được các vấn đề từ đoạn code ban đầu. Chúng ta thấy rằng thậm chí chúng ta không cần phải tạo các hàm khác để thực hiện việc dọn dẹp code. Như chúng tôi đã nhắc đến từ trước đó, ý tưởng “one task at time” có thể giúp bạn dọn sạch mã bất kể cá ranh giới của hàm.

Tuy nhiên chúng tôi đã cải thiện đoạn code này theo cách khác, bằng cách sử dụng 3 hepler:

1
2
3
4
5
void UpdateCounts(HttpDownload hd) {
counts["Exit State"][ExitState(hd)]++;
counts["Http Response"][HttpResponse(hd)]++;
counts["Content-Type"][ContentType(hd)]++;
}

Các hàm này sẽ trích xuất các giá trị tương ứng hoặc trả về “unknown”, ví dụ:
1
2
3
4
5
6
7
string ExitState(HttpDownload hd) {
if (hd.has_event_log() && hd.event_log().has_exit_state()) {
return ExitStateTypeName(hd.event_log().exit_state());
} else {
return "unknown";
}
}

Cách xử lý thay thế này không cần định nghĩa 1 vài biến. Và như chúng tôi đã nhắc đến trong Chapter 9, Variables and Readability, những biến giữ các giá trị trung gian nên được loại bỏ hoàn toàn.

Trong giải pháp này, chúng tôi đã đơn giản “cắt” các vấn đề vào trong các đường dẫn khác nhau. Cả 2 giải pháp đều dễ đọc và chúng yêu cầu người đọc nghĩ về 1 task trong 1 thời điểm.

Summary

Chương này minh họa 1 kỹ thuật đơn giản để tổ chức lại code của bạn: làm 1 task trong 1 thời điểm.

Nếu bạn có các đoạn code khó đọc, hãy thử liệt kê những task nó đang làm. Một vài tasks này có thể dễ dàng được tách thành các hàm (hoặc lớp) riêng biệt. Các tasks khác có thể trở thành “đoạn văn” trong 1 hàm đơn. Tách các phần chi tiết các task đang thực hiện không quan trọng bằng thực tế chúng đã được tách biệt ra. Phần khó nhất là mô tả được chính xác tất cả các thứ chương trình của bạn đang thực hiện.

The exact details of how you separate these tasks isn’t as important as the fact that they’re separated. The hard part is accurately describing all the little things your program is doing.

Author

Ming

Posted on

2020-05-24

Updated on

2024-08-08

Licensed under

Comments