Chapter 8: Breaking Down Giant Expressions

Một con mực khổng lồ là loài động vật tuyệt vời và thông minh nhưng thiết kế body của nó gần hoàn hảo có 1 lổ hổng chết người: nó có 1 cái não mềm dẻo (donut-shaped brain) bọc trong thực quản. Do đó nếu nó nuốt quá nhiều thức ăn trong 1 lần, não của nó sẽ hư hại.

Điều này làm chúng ta phải làm gì với code? Well, code đến từ các “chunks” cái mà quá lớn, có thể có cùng hiệu ứng. Các nghiên cứu gần đây gợi ý rằng đa số chúng ta chỉ có thể nghĩ về 3 hoặc 4 “thứ” trong cùng 1 lúc. (Ref: Cowan, N. (2001). The magical number 4 in short-term memory: A reconsideration of mental storage capacity. Behavioral and Brain Sciences, 24, 97–185.). Chỉ cần đặt đơn giản, 1 biểu thức lớn hơn trong code, đã rất khó hơn để hiểu.

K E Y I D E A
Break down your giant expressions into more digestible pieces.

1
Trong chương này, chúng ta sẽ suy nghĩ 1 vài cách bạn có thể vận dụng và ngắt các đoạn code của bạn để dễ dàng nuốt được =)).

Explaining Variables

Cách đơn giản nhất để phá vỡ các biểu thức là giới thiệu thêm biến cái mà nhận từ các biểu thức con nhỏ hơn. Các biến thêm này đôi khi được gọi là “explaining variable”.

Đây là 1 ví dụ:

1
2
if line.split(':')[0].strip() == "root":
...

Cũng là đoạn code trên nhưng bây giờ sử dụng các biến giải thích:
1
2
3
username = line.split(':')[0].strip()
if username == "root":
...

Summary Variables

Ngay cả khi 1 biểu thức không cần giải thích (vì bạn đã hiểu ý nghĩa của nó) nó có thể vẫn hữu ích để ghi lại biểu thức này ra 1 biến mới. Chúng ta gọi nó là summary variable nếu mục đích của nó đơn giản là thay thế một khối chunk lớ trong code bằng 1 cái tên nhỏ hơn, cái có được quản lý và nghĩ đơn giản hơn.

Ví dụ, cân nhắc biểu thức sau trong đoạn code này:

1
2
3
4
5
6
7
if (request.user.id == document.owner_id) {
// user can edit this document...
}
...
if (request.user.id != document.owner_id) {
// document is read-only...
}

Biểu thức request.user.id == document.owner_id có thể không lớn nhưng nó có 5 biến, do đó, mất 1 chút thời gian để nghĩ về nó.

Nội dung chính của đoạn code này là “Người dùng này có phải là chủ của tài liệu?”. Nội dung có thể được trình bày chi tiết hơn bằng cách thêm 1 summary variable:

1
2
3
4
5
6
7
8
9
10
final boolean user_owns_document = (request.user.id == document.owner_id);

if (user_owns_document) {
// user can edit this document...
}

...
if (!user_owns_document) {
// document is read-only...
}

Nó có vẻ không nhiều nhưng đoạn code if (user_owns_document) lại có 1 chút dễ hiểu hơn. Bằng cách này, có 1 định nghĩa user_owns_document ở đầu nói cho người đọc trước rằng “đây là khái niệm chúng ta sẽ tham chiếu trong suốt hàm này”.

Using De Morgan’s Laws

Nếu bạn đã từng tham gia 1 khóa học về đại số, bạn chắc hẳn vẫn còn nhớ nguyên tắc De Morgan. Chúng ta có 2 cách để viết lại 1 biểu thức boolean vào trong 1 biểu thức tương đương.

1
2
1) not (a or b or c) ⇔ (not a) and (not b) and (not c)
2) not (a and b and c) ⇔ (not a) or (not b) or (not c)

Nếu bạn có 1 vấn đề để nhớ về nguyên tắc này, 1 thứ đơn giản hơn “Chia not và chuyển đổi and/or” (Hoặc 1 cách khác, bạn đặt “thừa số chung cho not”)

Đôi khi bạn có thể sử dụng nguyên tắc này để làm cho biểu thức boolean của bạn dễ đọc hơn. Ví dụ nếu code của bạn:

1
if (!(file_exists && !is_protected)) Error("Sorry, could not read file.");

Nó nên được viết lại thành:
1
if (!file_exists || is_protected) Error("Sorry, could not read file.");

Abusing Short-Circuit Logic

Trong đa số các ngôn ngữ lập trình, toán tử boolean thực hiện đánh giá ngắn (short-circuit). Ví dụ, câu lệnh if (a || b) không được xem xét b nữa nếu a là true. Hành vi này rất tiện dụng nhưng đôi khi nó có thể bị lạm dụng để thực hiện các logic phức tạp.

Đây là 1 ví dụ:

1
assert((!(bucket = FindBucket(key))) || !bucket->IsOccupied());

Trong tiếng Anh, đoạn code này đang nói rằng “Lấy bucket với key này. Nếu bucket không null, sau đó hãy chắc chắc rằng nó chưa bị chiếm”

Mặc dù nó chỉ cần 1 dòng code nhưng thực sự làm cho ai đó đọc code của bạn phải dừng lại và nghĩ. Bây giờ so sánh với đoạn code này:

1
2
bucket = FindBucket(key);
if (bucket != NULL) assert(!bucket->IsOccupied());

Mọi thức là như nhau và thậm chí mất tới 2 dòng để code, nhưng nó dễ hiểu hơn nhiều.

Vậy tại sao đoạn code với biểu thức khổng lồ lại ở vị trí đó đầu tiên? Lúc đó, nhìn nó rất thông minh và ngầu. Có 1 niềm vui nào đó trong việc đã phân tích được logic xuống 1 đoạn mã ngắn gọn. Nó có thể hiểu được - nó giống như giải 1 câu đố nhỏ và tất cả chúng ta đều vui vẻ với nó. Vấn đề là đoạn code này là 1 vấn đề về tốc độ tinh thần cho ai đó đọc qua code của bạn.

K E Y I D E A
Beware of “clever” nuggets of code—they’re often confusing when others read the code later.

Thế có nghĩa là chúng ta nên tránh sử dụng các hành vi short-circuit? Không. Có rất nhiều trường hợp nó được sử dụng rất hữu ích và rõ ràng, như ví dụ sau:

1
if (object && object->method()) ...

Đây cũng là 1 điều đang nói đến: trong các ngôn ngữ như Python, JavaScript, Ruby, toán tử “or” trả về 1 trong các đối số của nó (nó không chuyển đổi sang boolean), do đó code của bạn:

1
x = a || b || c

có thể được sử dụng để chọn 1 giá trị “đúng” từ 3 giá trị a,b, hoặc c

Example: Wrestling with Complicated Logic

Giả sử bạn đang thực thi 1 lớp Range:

1
2
3
4
5
6
struct Range {
int begin;
int end;
// For example, [0,5) overlaps with [3,8)
bool OverlapsWith(Range other);
};

Hình dưới chỉ ra 1 vài phạm vi ví dụ:

Nhận thấy rằng end là không bao gồm. Do đó A,B và C không chồng chéo với nhau nhưng D lại chồng chéo lên tất cả.

Đây là 1 sự cố gắng thực thi OverlapsWith() - nó kiểm tra nếu 1 trong 2 điểm cuối của có trong cùng khác:

1
2
3
4
5
bool Range::OverlapsWith(Range other) {
// Check if 'begin' or 'end' falls inside 'other'.
return (begin >= other.begin && begin <= other.end) ||
(end >= other.begin && end <= other.end);
}

Mặc dù đoạn code chỉ có 1 dòng, nhưng có rất nhiều điều đang diễn ra. Hình dưới chỉ ra tất cả các logic tham gia vào.

Có rất nhiều trường hợp và điều kiện để nghĩ rằng nó dễ dàng bị bug lọt qua

Nói về mà, đó là 1 bug. Code phần trước sẽ yêu cầu Range [0,2) chồng chéo lên [2,4) trong khi thực tế không phải vậy.

Vấn đề này bạn phải cẩn thận khi so sánh giá trị begin/end sử dụng <= hoặc chỉ <. Đây là cách sửa vấn đề này:

1
2
return (begin >= other.begin && begin < other.end) ||
(end > other.begin && end <= other.end);

Bây giờ nó đã đúng? Thực tế, nó vẫn còn 1 bug khác. Đoạn cdoe này bỏ qua trường hợp begin/end hoàn toàn bao quát 1 thứ khác.

Đây là cách fix trường hợp này:

1
2
3
return (begin >= other.begin && begin < other.end) ||
(end > other.begin && end <= other.end) ||
(begin <= other.begin && end >= other.end);

Yikes - đoạn code này đã trở thành cách phức tạp. Bạn không thể mong đợi ai đó đọc đoạn code này và tự tin biết rằng nó đúng. Vậy chúng ta làm gì bây giờ? Chúng ta có thể ngắn biểu thức siêu to khổng lồ này như thế nào?

Finding a More Elegant Approach

Có 1 khoảng thời gian bạn nên dừng lại và cân nhắc những cách tiếp cận khác nhau. Khi bắt đầu như với 1 vấn đề đơn giản (kiểm tra 2 khoảng có bị ghi đè) đã trở thành 1 phần logic cuốn lại với nhau đáng ngạc nhiên. Điều này thường là 1 dấu hiệu có 1 cách đơn giản hơn.

Nhưng tìm được 1 giải pháp thanh lịch cần sự sáng tạo. Làm sao để bạn biết nó? Một kĩ thuật để tiếp cận nếu bạn có thể giải quyết vấn đề với cách “ngược lại”. Dựa vào vấn đề bạn đang gặp phải, điều này có thể có nghĩa lặp các mảng theo chiều ngược lại hoặc điền vào 1 số cấu trúc dữ liệu lạc hậu hơn là chuyển tiếp.

Ở đây ngược lại với OverlapsWith() là “đừng chồng chéo lên nhau”. Để xem nếu 2 khoảng không chồng chéo hóa ra và 1 vấn đề đơn giản hơn, bởi vì chỉ có 2 khả năng:

  1. Phạm vi phần khác kết thúc trước bắt đầu của phần còn lại
  2. Phạm vi phần khác bắt đầu sau khi phần còn lại

Chúng ta có thể biến đoạn mã này thành dễ dàng như sau:

1
2
3
4
5
6
bool Range::OverlapsWith(Range other) {
if (other.end <= begin) return false; // They end before we begin
if (other.begin >= end) return false; // They begin after we end

return true; // Only possibility left: they overlap
}

Mỗi dòng code đơn giản - nó chỉ cần 1 phép toán so sánh duy nhất. Điều này cho phép người đọc đủ trí tuệ (brainpower) để tập trung vào chỗ <= là đúng.

Breaking Down Giant Statements

Chương này đang nói về phá vỡ các biểu thức riêng biệt, nhưng kĩ thuật tương tự cũng được áp dụng để phá vỡ các statements. Ví dụ, đoạn mã dưới đây có nhiều thứ đưa vào 1 lúc:

1
2
3
4
5
6
7
8
9
10
11
12
var update_highlight = function (message_num) {
if ($("#vote_value" + message_num).html() === "Up") {
$("#thumbs_up" + message_num).addClass("highlighted");
$("#thumbs_down" + message_num).removeClass("highlighted");
} else if ($("#vote_value" + message_num).html() === "Down") {
$("#thumbs_up" + message_num).removeClass("highlighted");
$("#thumbs_down" + message_num).addClass("highlighted");
} else {
$("#thumbs_up" + message_num).removeClass("highighted");
$("#thumbs_down" + message_num).removeClass("highlighted");
}
};

Biểu thức riêng lẻ trong đoạn code này không lớn nhưng khi đặt chúng cạnh nhau, nó tạo thành 1 statement khổng lồ đập vào bạn trong cùng 1 lúc.

May mắn thay, rất nhiều biểu thức này giống nhau, điều đó có nghĩ chúng ta có thể thêm 1 biến chung trên đầu của hàm (nó cũng là 1 cách thực thi nguyên lý DRY - Don’t Repeat Yourself)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var update_highlight = function (message_num) {
var thumbs_up = $("#thumbs_up" + message_num);
var thumbs_down = $("#thumbs_down" + message_num);
var vote_value = $("#vote_value" + message_num).html();
var hi = "highlighted";

if (vote_value === "Up") {
thumbs_up.addClass(hi);
thumbs_down.removeClass(hi);
} else if (vote_value === "Down") {
thumbs_up.removeClass(hi);
thumbs_down.addClass(hi);
} else {
thumbs_up.removeClass(hi);
thumbs_down.removeClass(hi);
}
};

Sự sáng tạo của var hi = "highlighted" không thực sự cần thiết nhưng nó cũng là 6 lần copy, có những lợi ích hấp dẫn:

  • Nó giúp tránh các lỗi gõ phím (Trên thực tế, trong ví dụ đầu tiên, bạn có thấy sai “highighted” ở trường hợp 5)
  • Nó thu nhỏ chiều rộng dòng hơn, làm code dễ dàng đọc lướt qua
  • Nếu tên lớp cần thay đổi, bạn chỉ cần đặt vào 1 chỗ để thay đổi nó.

Another Creative Way to Simplify Expressions: Một cách sáng tạo hơn để đơn giản hóa các biểu thức

Đây là 1 ví dụ khác với rất nhiều thứ diễn ra trong mỗi biểu thức, lần này là trong C++

1
2
3
4
5
6
7
8
void AddStats(const Stats& add_from, Stats* add_to) {
add_to->set_total_memory(add_from.total_memory() + add_to->total_memory());
add_to->set_free_memory(add_from.free_memory() + add_to->free_memory());
add_to->set_swap_memory(add_from.swap_memory() + add_to->swap_memory());
add_to->set_status_string(add_from.status_string() + add_to->status_string());
add_to->set_num_processes(add_from.num_processes() + add_to->num_processes());
...
}

Một lần nữa, mắt của bạn phải đối diện với mã dài và tương tự nhau nhưng không thực sự giống nhau. Sau 10s với sự xem xét cẩn thận, bạn nhận thấy mỗi dòng làm cái gì đó giống nhau, chỉ khác nhau mỗi trường:
1
add_to->set_XXX(add_from.XXX() + add_to->XXX());

Và trong C++, bạn có thể định nghĩa 1 marco để làm việc này:
1
2
3
4
5
6
7
8
9
10
11
void AddStats(const Stats& add_from, Stats* add_to) {
#define ADD_FIELD(field) add_to->set_##field(add_from.field() + add_to->field())

ADD_FIELD(total_memory);
ADD_FIELD(free_memory);
ADD_FIELD(swap_memory);
ADD_FIELD(status_string);
ADD_FIELD(num_processes);
...
#undef ADD_FIELD
}

Bây giờ chúng ta đã bỏ đi các sự lộn xộn, bạn có thể nhìn thấy code và lập tức hiểu bản chất thứ đang diễn ra. Nó rất rõ ràng cho mỗi dòng làm 1 thứ gì đó giống nhau.

Lưu ý rằng, chúng tôi không ủng hộ việc sử dụng marcos thường xuyên - trên thực tế, chúng ta thường xuyên tránh sử dụng chúng vì bạn có thể làm code hiểu nhầm và bị các lỗi tinh tế. Nhưng thỉnh thoảng, trong trường hợp này, nó đơn giản và chỉ cung cấp lợi ích rõ ràng cho việc đọc code.

Summary

Biểu thức khổng lồ làm ta khó khi nghĩ về nó. Chương này chỉ ra 1 vài cách ngắt chúng nhỏ đi để người đọc có thể “tiêu hóa” nó thành từng phần.

Một kĩ thuật đơn giản được giới thiệu là “explaining variables” cái mà nhận giá trị của 1 vài biểu thức con lớn. Tiêu chuẩn này có 3 lợi ích:

  • Ngắt các biểu thức lớn thành từng phần
  • Nó là tài liệu của code bằng cách mô tả các biểu thức con với 1 cái tên cô đọng.
  • Nó giúp người đọc xác định được “các khái niệm” chính trong code.

Một kỹ thuật khác để vận dụng logic của bạn, sử dụng nguyên lý De Morgan - kĩ thuật này có thể đôi khi viết lại các biểu thức boolean với các sạch sẽ hơn (ví dụ if (!(a && !b)) có thể trở thành if(!a || b)))).

Chúng tôi cũng chỉ ra 1 điều kiện logic phức tạp có thể phá vỡ thành các statement nhỏ như if (a < b) .... Thực tế tất cả các ví dụ code đã cải thiện trong chương này,có statement if không quá 2 giá trị bên trong. Thiết lập này là ý tưởng. Nó có thể không phải lúc nào cũng đơn giản để thực hiện - đôi khi nó yêu cầu “phủ nhận” vấn đề hoặc cân nhắc hướng ngược lại mục đích của bạn

Cuối cùng, mặc dù chương này nói về phá vỡ các biểu thức riêng lẻ, kĩ thuật tương tự này thường xuyên áp dụng có các khối code lớn. Bởi vậy, hãy tích cức phá vỡ các logic phức tạp bất cứ khi nào bạn thấy nó.

Author

Ming

Posted on

2020-05-24

Updated on

2021-04-10

Licensed under

Comments