Chapter 4: Aesthetics

Hãy nghĩ 1 chút về layouts (bố cục) của 1 tờ tạp chí - độ dài đoạn văn, chiều rộng của các cột, thứ tự của bài viết và bìa. A good magazine makes it easy to skip around from page to page, but also easy to read straight through..

Code tốt nên dễ nhìn (easy on the eyes). Trong chương này chúng ta sẽ chỉ ra cách sử dụng space, căn chỉnh và sắp xếp code cho dễ đọc

Chi tiết, có 3 nguyên tắc chúng ta sẽ sử dụng

  • Sử dụng bố cục thống nhất với mẫu chung là người đọc có thể sử dụng được
  • Làm cho các đoạn mã tương tự nhìn tương tự nhau
  • Nhóm các đoạn code liên quan với nhau thành các khối code

Cách thay đổi này dễ dàng thực hiện mà nó cải thiện đáng kể khả năng đọc code của bạn đó <3

AESTHETICS VS. DESIGN

In this chapter, we’re concerned only with simple “aesthetic” improvements you can make to your code. These types of changes are easy to make and often improve readability quite a bit. There are times when larger refactoring of your code (such as splitting out new functions or classes) can help even more. Our view is that good aesthetics and good design are independent ideas; ideally you should strive for both.

Why Do Aesthetics Matter?

Thư giãn 1 chút, bạn có muốn mình là nhân vật trong bức ảnh này :D.

Thử tưởng tượng code của bạn:

1
2
3
4
5
6
7
8
9
10
11
12
13
class StatsKeeper {
public:
// A class for keeping track of a series of doubles
void Add(double d); // and methods for quick statistics about them
private: int count;
/* how many so far
*/ public:
double Average();
private: double minimum;
list<double>
past_items
;double maximum;
};

Nó sẽ rất khó để hiểu thay vì bạn clean code thay thế như sau:
1
2
3
4
5
6
7
8
9
10
11
12
13
// A class for keeping track of a series of doubles
// and methods for quick statistics about them.
class StatsKeeper {
public:
void Add(double d);
double Average();
private:
list<double> past_items;
int count; // how many so far

double minimum;
double maximum;
};

Chắc chắn dễ dàng hơn khi làm việc với các đoạn code có thẩm mỹ. Nếu bạn đang nghĩ về nó, phần lớn thời gian lập trình của bạn dành cho việc nhìn code! Sẽ nhanh hơn bạn có thể đọc lướt qua code, dễ dàng với mọi người khi sử dụng nó nữa.

Sắp xếp lại các ngắt dòng để được nhất quán và nhỏ gọn

Giả sử bạn đang viết code Java tính toán hành vi chương trình của bạn hoạt động với 1 vài tốc độ mạng khác nhau. Bạn có TcpConnectionSimulator nhận vào 4 tham số trong hàm tạo:

  1. Tốc độ kết nối (Kbps)
  2. Độ trễ trung bình (ms)
  3. The “jitter” của độ trễ (ms)
  4. Tỉ lệ mất gói tin (phần trăm)

Đoạn code của bạn cần 3 instance TcpConnectionSimulator khác nhau:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class PerformanceTester {
public static final TcpConnectionSimulator wifi = new TcpConnectionSimulator(
500, /* Kbps */
80, /* millisecs latency */
200, /* jitter */
1 /* packet loss % */);

public static final TcpConnectionSimulator t3_fiber =
new TcpConnectionSimulator(
45000, /* Kbps */
10, /* millisecs latency */
0, /* jitter */
0 /* packet loss % */);
public static final TcpConnectionSimulator cell = new TcpConnectionSimulator(
100, /* Kbps */
400, /* millisecs latency */
250, /* jitter */
5 /* packet loss % */);
}

Đoạn code cần rất nhiều kí tự xuống dòng bổ sung để phù hợp với yêu cầu giới hạn 80 kí tự trên 1 dòng (tiêu chuẩn của công ty). Không may mắn, chúng ta nhìn dòng định nghĩa t3_fiber nhìn rất khác với các “hàng xóm” của nó. Điều này có nghĩa nó đang không theo nguyên tắc “similar code should look similar”.

Để làm cho các đoạn code của bạn thống nhất, chúng ta có thể giới thiệu thêm các dấu ngắt dòng (và xếp hàng các comment khi chúng ta làm việc với nó):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class PerformanceTester {
public static final TcpConnectionSimulator wifi =
new TcpConnectionSimulator(
500, /* Kbps */
80, /* millisecs latency */
200, /* jitter */
1 /* packet loss % */);

public static final TcpConnectionSimulator t3_fiber =
new TcpConnectionSimulator(
45000, /* Kbps */
10, /* millisecs latency */
0, /* jitter */
0 /* packet loss % */);

public static final TcpConnectionSimulator cell =
new TcpConnectionSimulator(
100, /* Kbps */
400, /* millisecs latency */
250, /* jitter */
5 /* packet loss % */);
}

Đoạn code đã có mẫu thống nhất đẹp và dễ dàng để quét qua. Nhưng không may, nó sử dụng rất nhiều dấu cách. Nó cũng lặp comment 3 lần.

Đây là cách gọn nhẹ hơn để viết class trên

1
2
3
4
5
6
7
8
9
10
public class PerformanceTester {
// TcpConnectionSimulator(throughput, latency, jitter, packet_loss)
// [Kbps] [ms] [ms] [percent]
public static final TcpConnectionSimulator wifi =
new TcpConnectionSimulator(500, 80, 200, 1);
public static final TcpConnectionSimulator t3_fiber =
new TcpConnectionSimulator(45000, 10, 0, 0);
public static final TcpConnectionSimulator cell =
new TcpConnectionSimulator(100, 400, 250, 5);
}

Chúng ta đã di chuyển comment lên top và sau đó để tất cả các tham số vào 1 dòng. Bây giờ, thậm chí comment không còn cho mỗi dòng, “dữ liệu” được sắp xếp thành 1 bảng gọn nhẹ hơn.

Use Methods to Clean Up Irregularity: Sử dụng các phương thức để dọn dẹp các thứ bất thường

Giả định bạn có 1 cơ sở dữ liệu cung cấp các hàm như sau:

1
2
3
// Turn a partial_name like "Doug Adams" into "Mr. Douglas Adams".
// If not possible, 'error' is filled with an explanation.
string ExpandFullName(DatabaseConnection dc, string partial_name, string* error);

Và hàm này được test với 1 vài ví dụ:
1
2
3
4
5
6
7
8
9
10
11
12
DatabaseConnection database_connection;
string error;
assert(ExpandFullName(database_connection, "Doug Adams", &error)
== "Mr. Douglas Adams");
assert(error == "");
assert(ExpandFullName(database_connection, " Jake Brown ", &error)
== "Mr. Jacob Brown III");
assert(error == "");
assert(ExpandFullName(database_connection, "No Such Guy", &error) == "");
assert(error == "no match found");
assert(ExpandFullName(database_connection, "John", &error) == "");
assert(error == "more than one result");

Đoạn code này không có tính chất thẩm mỹ. Một vài dòng quá dài và chèn xuống cả dưới. Đoạn code xấu xí và không theo 1 mẫu nhất quán.

Nhưng đây là 1 trường hợp mà sắp xếp lại các dấu ngắt dòng như trên sẽ có rất nhiều. Vấn đề lớn hơn là có rất nhiều các string lặp lại như assert(ExpandFullName(database_connection... ," và “error” cái mà nếu cố theo cách này. Để thực sự cải thiện đoạn code này, chúng ta cần 1 phương thức helper để code có thể nhìn như:

1
2
3
4
CheckFullName("Doug Adams", "Mr. Douglas Adams", "");
CheckFullName(" Jake Brown ", "Mr. Jake Brown III", "");
CheckFullName("No Such Guy", "", "no match found");
CheckFullName("John", "", "more than one result");

Bây giờ, mọi thứ đã rõ ràng hơn, có 4 tests đang diễn ra, mỗi test với tham số khác nhau. Mặc dù tất cả các “dirty work” đã bên trong CheckFullName(), hàm này cũng không quá tệ

1
2
3
4
5
6
7
8
9
void CheckFullName(string partial_name,
string expected_full_name,
string expected_error) {
// database_connection is now a class member
string error;
string full_name = ExpandFullName(database_connection, partial_name, &error);
assert(error == expected_error);
assert(full_name == expected_full_name);
}

Mặc dù mục đích của chúng ta là cho code có thẩm mỹ hơn, điều này có 1 vài lợi ích khác:

  • Nó loại bỏ rất nhiều các đoạn code lặp lại trước đó, làm cho code nhỏ gọn hơn.
  • Phần quan trọng của mỗi test case (tên và chuỗi error) bây giờ đã được bọc lại, dễ theo dõi. Trước đó, các chuỗi này xem kẽ với các dấu hiệu riêng như database_connectionerror
  • Bây giờ, thêm những test mới sẽ trở nên dễ dàng hơn

Nội dung của câu chuyện là làm cho code “nhìn đẹp mắt” thường dẫn đến nhiều kết quả hơn chỉ là cải tiến về bề mặt (surface) - nó có thể giúp bạn có cấu trúc code tốt hơn

Use Column Alignment When Helpful: Sử dụng căn chỉnh cột khi hữu ích

Chỉnh sửa góc và các cột làm code của bạn dễ đọc hơn và có thể quét (scan) qua text - scan through text.

Đôi khi bạn có thể giới thiệu “căn chỉnh cột” để làm code dễ đọc hơn. Ví dụ, như phần trước bạn có thể thêm dấu cách, xếp hàng cho các đối số của hàm:

1
2
3
4
CheckFullName("Doug Adams"      , "Mr. Douglas Adams"   , "");
CheckFullName(" Jake Brown " , "Mr. Jake Brown III" , "");
CheckFullName("No Such Guy" , "" , "no match found");
CheckFullName("John" , "" , "more than one result");

Trong đoạn code này, đã dễ dàng phân biệt được đối số thứ 2 và thứ 3 của hàm CheckFullName()

Đây là 1 ví dụ đơn giản lớp 1 tập lớn các biến được định nghĩa:

1
2
3
4
5
6
# Extract POST parameters to local variables
details = request.POST.get('details')
location = request.POST.get('location')
phone = equest.POST.get('phone')
email = request.POST.get('email')
url = request.POST.get('url')

Bạn có nhận thấy, định nghĩa thứ 3 lỗi chính tả, là (equest thay vì request). Lỗi như vậy dễ phát hiện khi bạn mọi thứ được sắp xếp gọn gàng.

Trong wget codebase, các tùy chọn có sẵn của command-line (khoảng hơn 100 dòng) được liệt kê như dưới đây:

1
2
3
4
5
6
7
8
9
commands[] = {
...
{ "timeout", NULL, cmd_spec_timeout },
{ "timestamping", &opt.timestamping, cmd_boolean },
{ "tries", &opt.ntry, cmd_number_inf },
{ "useproxy", &opt.use_proxy, cmd_boolean },
{ "useragent", NULL, cmd_spec_useragent },
...
};

Tiêu chuẩn này làm danh sách rất dễ để đọc lướt qua và nhảy từ cột này sang cột khác.

Should You Use Column Alignment?

Căn cạnh các cột cung cấp “các hành lang trực quan” làm code dễ dàng quét qua. Nó là 1 một ví dụ tốt để “làm cho code tương tự nhau nhìn tương đương nhau”.

Nhưng một vài lập trình viên không thích nó. Một lý do là tạo thêm việc khi set up và maintain các căn chỉnh này. Một lý do khác là nó tạo ra “một sự thay đổi” lớn hơn khi tạo 1 thay đổi - 1 dòng thay đổi có nghĩa là 5 dòng khác cũng cần thay đổi (thường sẽ là cần chỉnh lại các khoảng trống)

Lời khuyên của chúng tôi là cứ thử nó. Theo kinh nghiệm của chúng tôi, nó không mất nhiều công sức mà lập trình viên phải sợ. Và nếu có, bạn có thể đơn giản là dừng lại.

Pick a Meaningful Order, and Use It Consistently

Có rất nhiều trường hợp có thứ tự trong code không ảnh hưởng đến tính đúng đắn. Ví dụ, đây là 1 cách khai báo biến có thể được viết theo 1 vài thứ tự:

1
2
3
4
5
details     = request.POST.get('details')
location = request.POST.get('location')
phone = request.POST.get('phone')
email = request.POST.get('email')
url = request.POST.get('url')

Trong các trường hợp như vậy, sẽ hữu ích hơn nếu bạn đặt chúng thành 1 thứ tự có nghĩa, không phải là ngẫu nhiên. Dưới đây là 1 vài ý tưởng:

  • Trùng với thứ tự của biến được sắp xếp trong các trường <input> tương ứng trong HTML form.
  • Sắp xếp chúng từ “quan trọng nhất” đến “ít quan trọng nhất”
  • Sắp xếp chúng theo thứ tự bảng chữ cái.

Khi bạn sắp xếp, bạn nên sử dụng thứ tự giống nhau xuyên suốt code của bạn. Như vậy thì tránh gây nhầm lẫn thay đổi thứ tự sau đó:

1
2
3
4
5
if  details:    rec.details     =   details
if phone: rec.phone = phone # Hey, where did 'location' go?
if email: rec.email = email
if url: rec.url = url
if location: rec.location = location # Why is 'location' down here now?

Organize Declarations into Blocks

Não của chúng ta tự nhiên sẽ nghĩ các khái niệm trong nhóm và thứ bậc, do đó bạn giúp người đọc dễ dàng đọc code nhất theo cách này.

Ví dụ trong lớp C++ frontend server, với các phương thức được khai báo:

1
2
3
4
5
6
7
8
9
10
11
12
13
class FrontendServer {
public:
FrontendServer();
void ViewProfile(HttpRequest* request);
void OpenDatabase(string location, string user);
void SaveProfile(HttpRequest* request);
string ExtractQueryParam(HttpRequest* request, string param);
void ReplyOK(HttpRequest* request, string html);
void FindFriends(HttpRequest* request);
void ReplyNotFound(HttpRequest* request, string error);
void CloseDatabase(string location);
~FrontendServer();
};

Đoạn code này không gây sợ hãi nhưng cách bố trí không giúp người đọc phân loại được tất cả các phương thức. Thay vì liệt kê tất cả các phương thức trong 1 khối khổng lồ, chúng nên được tổ chức lại thành các nhóm, như sau:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class FrontendServer {
public:
FrontendServer();
~FrontendServer();

// Handlers
void ViewProfile(HttpRequest* request);
void SaveProfile(HttpRequest* request);
void FindFriends(HttpRequest* request);

// Request/Reply Utilities
string ExtractQueryParam(HttpRequest* request, string param);
void ReplyOK(HttpRequest* request, string html);
void ReplyNotFound(HttpRequest* request, string error);

// Database Helpers
void OpenDatabase(string location, string user);
void CloseDatabase(string location);
};

Phiên bản này dễ hiểu hơn nhiều khi đã được phân loại. Nó cũng dễ đọc hơn, mặc dù tốn nhiều dòng code hơn. Lý do bạn có thể dễ dàng tìm ra 1 phần high-level và sao đó đọc chi tiết mỗi phần nếu cần thiết.

Break Code into “Paragraphs”

Văn bản viết được chia thành các đoạn vì 1 số lý do sau:

  • Nó là các để nhóm các ý tưởng chung lại với nhau và đặt chúng cách nhau với các ý tưởng khác.
  • Nó cung cấp “bước đệm” trực quan- không có nó, bạn dễ dàng đánh mấy vị trí của bạn trên trang.
  • Nó tạo điều kiện để điều hướng từ 1 trang đến trang khác.

Code cũng nên tách thành các “đoạn văn” vì lý do như vậy. Ví dụ, không ai muốn đọc 1 đoạn code khổng lổ như vậy:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Import the user's email contacts, and match them to users in our system.
# Then display a list of those users that he/she isn't already friends with.
def suggest_new_friends(user, email_password):
friends = user.friends()
friend_emails = set(f.email for f in friends)
contacts = import_contacts(user.email, email_password)
contact_emails = set(c.email for c in contacts)
non_friend_emails = contact_emails - friend_emails
suggested_friends = User.objects.select(email__in=non_friend_emails)
display['user'] = user
display['friends'] = friends
display['suggested_friends'] = suggested_friends
return render("suggested_friends.html", display)

Nó có thể không rõ ràng, nhưng hàm này trải qua 1 số bước. Do đó nó đặc biệt hữu ích để ngắt các dòng này thành các đoạn văn:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def suggest_new_friends(user, email_password):
# Get the user's friends' email addresses.
friends = user.friends()
friend_emails = set(f.email for f in friends)

# Import all email addresses from this user's email account.
contacts = import_contacts(user.email, email_password)
contact_emails = set(c.email for c in contacts)

# Find matching users that they aren't already friends with.
non_friend_emails = contact_emails - friend_emails
suggested_friends = User.objects.select(email__in=non_friend_emails)

# Display these lists on the page.
display['user'] = user
display['friends'] = friends
display['suggested_friends'] = suggested_friends

return render("suggested_friends.html", display)

Nhận thấy rằng chúng ta cũng thêm các comment tóm tắt cho mỗi đoạn văn, cái giúp người đọc dễ đọc đọc qua code (Xem thêm chương 5 để biết những gì bạn nên comment)

Như khi viết văn, có nhiều cách để chia nhỏ code và lập trình viên có thể thích các đoạn văn dài hơn hoặc ngắn hơn =)).

Personal Style versus Consistency

Có 1 vài sự lựa chọn nhất định cho tính thẩm mĩ, cái mà nổi bọt hơn cả personal style. Ví dụ, dấu mở ngoặc khi định nghĩa 1 lớp nên là:

1
2
3
class Logger {
...
};

Hoặc
1
2
3
4
class Logger
{
...
};

Nếu 1 style nào hơn style nào, nó không ảnh hưởng đáng kể đến khả năng đọc code. Nhưng nếu có 2 style này trộn lẫn với nhau trong code, nó sẽ gây ra khó đọc code.

Chúng tôi đã làm việc với nhiều dự án mà chúng tôi cảm thấy team đang sử dụng “sai” style nhưng chúng tôi cho phép các quy ước này trong project vì chúng tôi biết rằng tính nhất quán quan trọng hơn nhiều.

KEY IDEA
Consistent style is more important than the “right” style.


Đối với mọi người nhìn vào, nó là 1 cái phòng trọ nhưng với những chú lợn thì nó chỉ là 1 cái chuồng lợn :D

Tóm lại

Mọi người thích đọc code có thẩm mỹ. Bằng “formatting” code theo 1 sự thống nhất, 1 cách có nghĩa, bạn sẽ làm nó đọc dễ dàng và nhanh chóng hơn.

Dưới đây là 1 số kĩ thuật cụ thể chúng ta đã thảo luận:

  • Nếu nhiều khối code đang làm những thứ tương tự nhau, hãy để chúng hình có vẻ giống nhau :D.
  • Căn chỉnh các phần của code vào trong các “cột” để làm code dễ dàng có thể đọc lướt qua.
  • Nếu code của bạn nhắc tới A,B và C trong 1 chỗ, đừng nói B,C và A ở 1 chỗ khác. Chọn 1 thứ tự có nghĩa và gắn bó với nó.
  • Sử dụng các dòng trống để phá vỡ các khối code lớn vào các “đoạn văn” cùng logical

Tài liệu tham khảo

Chapter 4 - The Art of Readable Code

Author

Ming

Posted on

2020-05-24

Updated on

2021-04-10

Licensed under

Comments