Readability

Code should be easy to understand

1. Tại sao?

Pasted image 20230209214519.png
Như ở phần Pragmatic Programmer trước, điều mà một developer mang lại là giá trị, và đồng thời giảm chi phí bỏ ra. Một trong những chi phí hay gặp là human resource.
Mất bao nhiêu thời gian đọc hiểu source để implement tính năng mới này? Và có an toàn không khi implement như vậy. Mất bao lâu để người mới làm quen, hiểu được codebase.

Vậy mình có thể rút ra mục tiêu con là giảm thời gian đọc hiểu source code.

Và câu hỏi nhỏ là: đối tượng mục tiêu là ai? ai sẽ là người đọc?
Để code mang tính khách quan, và tránh bị ảnh hưởng bởi experties của expert. Nên đối tượng cần nhắm tới là member mới.

Đa phần code đều đọc được và hiểu được, vì nó execute dựa trên một rule định sẵn. Và điều mình quan tâm không phải là code có đọc hiểu được hay không? mà là mất bao lâu.

2. Đặt tên

Một trong những điều khó khăn cho developer là đặt tên biến. Việc nghĩ ra một cái tên cho biến có khi ta đau đầu, để rồi kết quả là tmp một tên biến rất nỗi quen thuộc. Việc đặt tên tmp hoặc các từ khác không mang ý nghĩa thực sự của biến sẽ làm cho người đọc khó nắm bắt được flow và mục đích của đoạn code đó.

Vậy phải làm sao để đặt tên có ý nghĩa?

Chọn từ phù hợp

Trước tiên là cần tránh chọn từ mang ý nghĩa chung chung. Mà nên chọn từ cụ thể cho mục đích của đoạn code.
Ví dụ:

def GetPage(url):
	pass

Từ get ở đây thường được mọi người hay dùng. Nhưng nó lại mang từ rất chung chung. Để tìm được từ phù hợp, ta cần hiểu được context của hàm trên. Mục đích của hàm này là gì? Nếu là tải page từ server, vậy thì dùng từ fetch hoặc download sẽ rõ nghĩa hơn. Nó cho biết page được lấy từ server, từ internet, không phải từ local.

Ngoài ra, còn có từ thông dụng có tính nhập nhằng và có thể gây nhầm lẫn, và yêu cầu người sử dụng phải hiểu được logic của hàm.

class BinaryTree {
	int Size();
	...
}

Với đoạn code trên, Size() theo bạn hiểu nó mang ý nghĩa gì? Số lượng node trong cây? Độ sâu của cây? Số lượng bytes để lưu trữ cây? Có thể thấy Size gây nhập nhằng về ý nghĩa, và dựa trên kiến thức, kinh nghiệm của người đọc sẽ có cách hiểu khác nhau.
Giải pháp có thể là chọn các từ cụ thể hơn như là Depth(), NumNode(), MemoryBytes().

Ngoài ra, cũng cần chọn từ thỏa mong đợi từ người dùng. Ví dụ hàm customList.size() thường chỉ nhẹ nhàng. Do đó, nếu logic của hàm là tính size của object mỗi lần gọi thì sẽ khiến người dùng bất ngờ. Họ không biết và sử gọi hàm nhiều lần ví dụ:

int i=0;
while(i < list.size()) {
	cout << list[i] << ",";
	i++;
}

Như trên, người dùng giả sử rằng get size là $O(1)$, nhưng thực chất logic là mỗi lần gọi nó lại đếm lại, do đó đoạn code trên trở thành $O(n^2)$. Bug này thường khó phát hiện, và chỉ khi data đạt ngưỡng nào đó gây chậm thì mới để ý thấy.

Tránh các biến tmp

Nên chọn các từ mô tả lại mục đích của biến đó. Ví dụ:

double euclidean_norm(vector<double> v)
{
	double tmp = 0.0;
	for(int i=0; i < v.size(); i++)
		tmp += v[i]*v[i];
	return sqrt(tmp);
}

Nó sẽ dễ hiểu hơn nếu thay tmp bằng sum_square chẳng hạn. Hãy đặt tên theo giá trị ý nghĩa của biến. Dưới đây là logic của hàm swap nơi mà biến tmp mang đúng ý nghĩa của nó.

if (right < left) 
{
	tmp = right;
	right = left;
	left = tmp;
}

Ví dụ sau là một trường hợp khác không nên sử dụng tmp.

std::string tmp = user.name();
tmp += " " + uer.phone_number();
tmp += " " + user.email();
...
template.set("user_info", tmp);

Mặc dù scope của biến tmp ở đây ngắn, chỉ vài dòng code, nhưng vẫn sẽ dễ hiểu hơn nếu đặt là user_info.

Loop Iterators

Thông thường các biến index trong hàm for loop, thường chỉ gồm 1 ký tự. Điều này thông dụng và mọi người đều hiểu nên vẫn không sao. Nhưng nếu có nhiều vòng loop lồng nhau, cỡ 3 vòng như ví dụ dưới đây, sẽ làm khó việc xác định biến nào mang ý nghĩa gì.

for(int i=0; i < clubs.size(); i++)
	for(int j=0; j < clubs[i].members.size(); j++)
		for(int k=0; k < users.size(); k++)
			if (clubs[i].members[k] == users[j])
				cout << "user[" << j << "] is in club[" << i << "]" << endl;

Khi đọc đoạn code này, bạn mất bao lâu để hiểu được ý nghĩa của nó. Đoạn code này dùng để in ra thông tin member nào thuộc club nào.
Đoạn code trên sẽ dễ đọc hơn nếu thay, i bằng club_i hoặc ci, tương tự cho jk thành mi, ui.
khi đó dòng code if sẽ khiến cho việc nhớ ý nghĩa của các biến index này hơn. Đỡ tốn thời gian kiểm trả lại ý nghĩa trên các dòng for.

if (clubs[ci].members[ui] == users[mi])
	...

Ưu tiên cụ thể hơn trừu tượng

Thế nào là trừu tượng. Một tên trừu tượng là khi nó có ý nghĩa khát quát, chung chung.
Ví dụ: khi chạy chương trình ta có lệnh npm run start:local, với mode này sẽ output debug log ra ngoài console, việc này khiến performance bị chậm đi, do đó trên cloud, ta không bật flag này. Việc ẩn ý nghĩa của việc output debug log vào từ start:local sẽ gây ra:

  • đối với new members, họ sẽ không biết ý nghĩa thực sử của run local này, và tại sao phải cần nó.
  • trong trường hợp muốn debug xem log trên cloud. Chạy chương trình với options local nghe có vẻ không hợp lý lắm. Có thể gây hiểu lầm, cloud mà lại bật flag local???
  • ngược lại thêm trường hợp muốn test performance dưới local, thì lúc này lại tắt run local.
    Việc chọn tên local trừu tường này gây ra nhiều nhập nhằng. Thay vì vậy, chọn tên phù hợp hơn như npm run start --verbose_level=debug sẽ rõ nghĩa hơn.

Nhưng nếu local không chỉ có mỗi output log thì sao? ví dụ setup local database chẳng hạn. Lúc này local không phải sẽ có nghĩa hơn sao, do nó bao gồm nhiều việc hơn. Cũng đúng, nhưng ta vẫn phải cân nhắc giá trị nó mang lại. Mất bao lâu, và ở đâu document lại option này bao gồm những tác dụng gì? Gom lại thành một option tốt hơn hay tách thành hai options riêng biệt thì tốt hơn (--verbose_level--use_local_database). Rõ ràng khi tách thành 2 option riêng, ta sẽ linh động hơn. Nhưng nếu có nhiều hơn 2 việc, 3, 4, 5 việc thì sao? Lúc này, ta cần phải xem xét, các bước này có thường xuyên phải sử dụng đơn lẻ, một tập nhỏ, để linh động có thể sử dụng config file.

Thêm thông tin đặc thù vào tên biến

Thông tin đặc thù có thể là đơn vị, encoding. Một số trường hợp nếu không thêm đơn vị có thể dẫn đến bug. Ví dụ như:

var start = (new Date()).getTime(); // top of the page 
...
var elapsed = (new Date()).getTime() - start; // bottom of the page
console.log("Load time was: " + elapsed + " seconds");

Chỉ dùng start và elapsed không sẽ không biết chính xác đơn vị của thời gian là gì, giờ, phút, giây hay mili giây. Do đó, để tránh xảy ra nhầm lẫn nên cho thêm thông tin đơn vị vào ví dụ như: start_ms, elapsed_ms hoặc rotate(float angle) thành rotate(float rad_cw) với cw là counter clockwise, do rotate sẽ còn có thông tin hướng xoay.

Một số loại thông tin khác như plaintext_password cho biết đây là dữ liệu password chưa được encripted.

Độ dài tên bao nhiêu là tốt?

Không thể xác định được tên dài bao nhiêu sẽ tốt. Nhưng tên dài quá thì nội thời gian đọc nó thôi đã mệt, đọc tới cuối sợ còn không nhớ khúc đầu của tên nó là cái gì.
Vậy với trường hợp đơn giản hơn là giữa dayd thì nên chọn cái nào? Việc này có thể dựa trên scope của biến.

scope nhỏ tên ngắn vẫn được

Với những biến có scope nhỏ, người đọc sẽ thấy được toàn life time của biến và vẫn có thể nhớ ý nghĩa của biến đó. Trong trường hợp scope bự, file cỡ 200 dòng, mà biến User đặt u tại đầu file, thì khi đọc ở giữa hoặc cuối file thấy dòng u.name() hay u.toJson() thì rất khó nhớ u này là cái gì.

Từ viết tắt thì sao?

Với những member đã hiểu dự án, các term hay dùng thường được viết tắt lại, ví dụ như MEManager, MEService đại diện cho MapEditorManager và MapEditorService. Hay RBMS3Service thay cho RoboMapS3Service.
Nếu phân vân hãy cứ để giá trị dẫn lối. Cố gắn tự hỏi:

liệu member mới có hiểu được tên đó hay không?

Nếu họ không hiểu được, vậy thời gian để giải thích mất bao lâu? Nếu nó là từ khóa chính, tên riêng, thì chắc chắn rất dễ hiểu.

Convention Format

Một số ngôn ngữ sẽ có convention format, ứng với nó là một ý nghĩa được sử dụng rộng rãi. Ví dụ như tên của MACRO trong C++ (#define) thường được viết hoa toàn bộ. Tên class, tên hàm dùng CamelCase. Với flutter thí có _name để chỉ biến private.

Ngoài ra, layout cũng quan trọng. Nên chia thành từng group, block con thay vì gom thành một cục gây khó đọc.

3. Comment

Mục tiêu của việc comment là gì? Là để mô tả lại đoạn code ở dưới nó?

comment là để giúp người đọc biết được nhiều thông tin như tác giả đoạn code.

Không chỉ mô tả lại logic đoạn code, mà nó nên ghi lại những assumption, những ghi chú mà ta thấy người đọc cần phải biết để hiểu được đoạn code.

Trước tiên của việc comment là không comment những thứ quá hiển nhiên. Bản thân code đã có thể mô tả được ý nghĩa của nó.

class User {
public:
	...
	// Set user's name 
	void SetName(string name);

	// Get the phone number of user
	string GetPhoneNumber();
}

Với đoạn code sau, mặc dù đọc dòng code bên dưới cũng sẽ hiểu được nhưng thời gian đọc hiểu sẽ lâu hơn, với việc đọc comment.

# remove everything after the second '*'
name = '*'.join(line.split('*')[:2])

Lưu ý, không lạm dụng comment để giải thích cho những tên biến mập mờ. Ví dụ như

  • The Fundamental Theorem of Readability
    • Code should be written to minimize the time it would take for someone else to understand it.
  • Is Smaller Always Better?
  • Does Time-Till-Understanding Conflict with Other Goals?
  • Surface-Level Improvements
    • Pack information into your names.
      • Choose Specific Words
      • Finding More “Colorful” Words
        • It’s better to be clear and precise than to be cute.
    • Avoid Generic Names Like tmp and retval
      • pick a name that describes the entity’s value or purpose
      • The name retval doesn’t pack much information. Instead, use a name that describes the variable’s value.
      • The name tmp should be used only in cases when being short-lived and temporary is the most important fact about that variable.
    • Loop Iterators
    • The Verdict on Generic Names
      • If you’re going to use a generic name like tmp, it, or retval, have a good reason for doing so
    • Prefer Concrete Names over Abstract Names
      • Example: --run_locally
      • Attaching Extra Information to a Name
        • Values with Units
        • Encoding Other Important Attributes
      • How Long Should a Name Be?
        • Shorter Names Are Okay for Shorter Scope
        • Use longer names for larger scopes
      • Acronyms and Abbreviation
        • rule of thumb is: would a new teammate understand what the name means? If so, then it’s probably okay
      • Throwing Out Unneeded Words
      • Use Name Formatting to Convey Meaning
      • Other Formatting Conventions
        • Use capitalization, underscores, and so on in a meaningful way
    • Names That Can’t Be Misconstrued
      • Actively scrutinize your names by asking yourself, “What other meanings could someone interpret from this name?”
      • Prefer min and max for (Inclusive) Limits
        • The clearest way to name a limit is to put max_ or min_ in front of the thing being limited.
      • Prefer begin and end for Inclusive/Exclusive Ranges
    • Naming Booleans
      • When picking a name for a boolean variable or a function that returns a boolean, be sure it’s clear what true and false really mean.
    • Matching Expectations of Users
      • Many programmers are used to the convention that methods starting with get are “lightweight accessors”
      • Example: Evaluating Multiple Name Candidates
    • Aesthetics
      • The faster you can skim through your code, the easier it is for everyone to use it
      • Rearrange Line Breaks to Be Consistent and Compact
      • Use Methods to Clean Up Irregularity
      • Use Column Alignment When Helpful
        • Should You Use Column Alignment?
      • Consistently
        • Meaningful Order
        • Personal Style versus Consistency
          • Consistent style is more important than the “right” style.
      • Organize Declarations into Blocks
  • Knowing What to Comment
    • The purpose of commenting is to help the reader know as much as the writer did.
    • What NOT to Comment
      • Don’t comment on facts that can be derived quickly from the code itself.
      • Don’t Comment Just for the Sake of Commenting
      • Don’t Comment Bad Names—Fix the Names Instead
    • Recording Your Thoughts
      • comment acknowledges that the code is messy but also encourages the next person to fix it (with specifics on how to get started). Without the comment, many readers would be intimidated by the messy code and afraid to touch it.
      • Comment the Flaws in Your Code
        • feel free to comment on your thoughts about how the code should change in the future. Comments like these give readers valuable insight into the quality and state of the code and might even give them some direction on how to improve it
      • Comment on Your Constants
        • When defining a constant, there is often a “story” for what that constant does or why it has that specific value
      • imagine what your code looks like to an outsider
        • What is surprising about this code? How might it be misused?
        • “Big Picture” Comments
          • someone new just joined your team, she’s sitting next to you, and you need to get her familiar with the codebase.
      • Summary Comments
        • comments also act as a bulleted summary of what the function does, so the reader can get the gist of what the function does before diving into details
      • Getting Over Writer’s Block
        • it feels like a lot of work to write a good one. When writers have this sort of “writer’s block,” the best solution is to just start writing.
  • Making Comments Precise and Compact
    • Comments should have a high information-to-space ratio.
      • Keep Comments Compact
      • Avoid Ambiguous Pronouns
      • Polish Sloppy Sentences
    • Describe Function Behavior Precisely
    • Use Input/Output Examples That Illustrate Corner Cases
    • State the Intent of Your Code
      • , commenting is often about telling the reader what you were thinking about when you wrote the code. Unfortunately, many comments end up just describing what the code does in literal terms, without adding much new information.
    • “Named Function Parameter” Comments
    • Use Information-Dense Words
    • Using common words can make your comments much more compact.

part 2: layout

  • Simplifying Loops and Logic
    • Making Control Flow Easy to Read
      • Make all your conditionals, loops, and other changes to control flow as “natural” as possible—written in a way that doesn’t make the reader stop and reread your code.
    • The Order of Arguments in Conditionals
    • The Order of if/else Blocks
      • Prefer dealing with the positive case first instead of the negative—e.g., if (debug) instead of if (!debug). •
      • Prefer dealing with the simpler case first to get it out of the way. This approach might also allow both the if and the else to be visible on the screen at the same time, which is nice.
      • Prefer dealing with the more interesting or conspicuous case first.
    • The ?: Conditional Expression (a.k.a. “Ternary Operator”)
      • Instead of minimizing the number of lines, a better metric is to minimize the time needed for someone to understand it
      • By default, use an if/else. The ternary ?: should be used only for the simplest cases
    • Returning Early from a Function
    • Minimize Nesting
      • How Nesting Accumulates
      • Look at your code from a fresh perspective when you’re making changes. Step back and look at it as a whole
      • Removing Nesting by Returning Early
      • Removing Nesting Inside Loops
    • Can You Follow the Flow of Execution?