Chapter 3: Names That Can’t Be Misconstrued
Trong chương trước, chúng ta đã khám phá việc làm sao để đóng gói các thông tin vào tên của bạn. Trong chương này, chúng ta sẽ tập trung vào 1 chủ đề khác: coi chừng những cái tên có thể bị hiểu nhầm
KEY IDEAD
Chủ động xem xét các tên của bạn bằng cách hỏi chính mình “liệu có nghĩa khác có thể làm cho ai đó giải thích từ tên này”
Thực sự bạn nên thử sự sáng tạo ở đây, tích cực tìm ra những “diễn giải sai”. Bước này giúp bạn phát hiện ra những cái tên mơ hồ để từ đó bạn có thể thay đổi chúng.
Example: Filter()
Giả sử bạn đang viết đoạn code dùng để lấy 1 tập kết quả trong database:1
results = Database.all_objects.filter("year <= 2011")
result
bây giờ chứa cái gì?
- Những đối tượng có năm <= 2011?
- Những đối tượng có năm không <= 2011?
Vấn đề là filter
là 1 từ mập mờ. Nó không rõ ràng như là “to pick out“ (lấy ra) hoặc “to get rid of“ (loại bỏ). Tốt nhất là bạn nên tránh dùng tên filter vì nó dễ gây hiểu nhầm.
Nếu bạn muốn “to pick out”, một cái tên tốt hơn sẽ là select()
. Nếu bạn muốn “to get rid of”, một cái tên tốt hơn sẽ là exclude()
.
Example: Clip(text, length)
Giả sử bạn có 1 hàm giữ thông tin của đoạn văn1
2
3# Cuts off the end of the text, and appends "..."
def Clip(text, length):
...
Có 2 cách hiểu bạn có thể tưởng tượng với hành động Clip()
:
- Nó sẽ xóa độ dài từ cuối.
- Nó cắt ngắn text đến độ dài tối đa.
Cách hiểu thử 2 sẽ chiếm đa số nhưng bạn cũng không biết chắc. Để cho sự hoài nghi không còn, bạn có thể sử dụng một cái tên tốt hơn là Truncate(text, length).
Tuy nhiên, tham số tên length
cũng bị đổ lỗi. Nếu nó mà max_length
, tên đã sẽ rõ ràng hơn.
Nhưng chúng ta vẫn chưa xong. Tên max_length
vẫn để lại 1 số cách hiểu khác:
- Số lượng bytes
- Số lượng kí tự
- Số lượng từ
Như bạn đã thấy từ chương trước, đây là 1 trường hợp mà đơn vị nên được thêm vào tên. Trong trường hợp này, chúng ta muốn sử dụng “số lượng kí tự”, do đó thay vì đặt tên là max_length
, hãy sử dụng max_chars
.
Prefer min and max for (Inclusive) Limits
Ứng dụng mua sẵn của bạn không cho phép người dùng mua quá 10 sản phẩm trong 1 lần shopping1
2
3CART_TOO_BIG_LIMIT = 10
if shopping_cart.num_items() >= CART_TOO_BIG_LIMIT:
Error("Too many items in cart.")
Đoạn code này bị 1 lỗi kinh điển off-by-one. Chúng ta có thể dễ dàng sửa nó bằng cách thay thế >= bằng >:1
if shopping_cart.num_items() > CART_TOO_BIG_LIMIT:
(Hoặc sửa CART_TOO_BIG_LIMIT lên 11). Nhưng vấn đề tận gốc là CART_TOO_BIG_LIMIT là 1 cái tên mơ hồ - nó không rõ ràng là bạn muốn “up to” (lớn hơn hẳn) hay “up to and including” (lớn hơn và bằng).
ADVICE
Cách rõ ràng nhất để đặt tên 1 giới hạn là đặt tên nó
max_
hoặcmin_
vào trước
Trong trường hợp này, tên nên là MAX_ITEMS_IN_CART. Code mới sẽ đơn giản và rõ ràng:1
2
3MAX_ITEMS_IN_CART = 10
if shopping_cart.num_items() > MAX_ITEMS_IN_CART:
Error("Too many items in cart.")
Prefer first
and last
for Inclusive Ranges
Đây là 1 ví dụ khác khi bạn không thể sử dụng lớn hơn hoặc lớn hơn hoặc bằng1
2print integer_range(start=2, stop=4)
# Does this print [2,3] or [2,3,4] (or something else)?
Mặc dù start
là 1 cái tên hợp lý nhưng stop
lại không hề rõ ràng và có thể có 1 vài cách hiểu.
Đối với các phạm vi bao gồm như vậy (nơi mà phạm vi nên bao gồm cả điểm bắt đầu và điểm kết thúc), một sự lựa chọn tốt với tên first/last
. Ví dụ1
set.PrintKeys(first="Bart", last="Maggie")
Không giống như stop
, từ last
ở đây đã rõ nghĩa.
Thêm vào đó, first/last
, tên min/max
cũng có thể hoạt động trong phạm vi bao gồm này miễn là nghĩa phù hợp trong bối cảnh.
Prefer begin
and end
for Inclusive/Exclusive Ranges
Trong quá trình code, thường xuyên chúng ta gặp các phạm vi bao gồm/loại trừ. Ví dụ, nếu bạn muốn in ra tất cả các sự kiện xảy ra vào 16 tháng 10, chúng ta dễ dàng viết như sau:1
PrintEventsInRange("OCT 16 12:00am", "OCT 17 12:00am")
Hơn là viết kiểu như:1
PrintEventsInRange("OCT 16 12:00am", "OCT 16 11:59:59.9999pm")
Do đó một cặp tên tốt với tham số này là gì? Well, theo quy ước của lập trình chung, tên của phạm vi bao gồm/loại trừ là begin/end
Nhưng từ end
có 1 chút chưa rõ nghĩa. Ví dụ, trong câu văn “I’m at the end of the book,”, từ “end“ là đã bao gồm. Không may, tiếng anh không có từ nào ngắn gọn thay thế cho cụm từ “just past the last value”
Bởi vì begin/end
cũng là quốc ngữ (ít nhất, nó đã được sử dụng trong các thư viện chuẩn cho C++ và hầy hết những nơi mà 1 mảng cần phải được “cắt” theo cách này), nó vẫn là 1 sự lựa chọn tốt nhất.
Naming Booleans
Khi chọn 1 tên cho giá trị biến boolean hoặc 1 hàm sẽ trả về boolean, hãy chắc chắn rằng nó rõ ràng true và false thực sự có nghĩa.
Đây là 1 ví dụ nguy hiểm:1
bool read_password = true;
Tùy thuộc vào cách bạn đọc nó (không tính chơi chữ), có 2 cách có thể hiểu:
- Bạn cần đọc password
- Password đã được đọc
Trong trường hợp này, cách tốt nhất để tránh từ “read” và đặt tên nó là need_password
hoặc user_is_authenticated
thay thế.
Nói chung, thêm những từ như is
, has
, can
, hoặc should
có thể làm cho các giá trị boolean
rõ ràng hơn.
Ví dụ, một hàm được đặt tên là SpaceLeft()
nhìn như nó sẽ trả về 1 số. Nếu nó có nghĩa là trả về boolean, một cái tên tốt hơn sẽ là HasSpaceLeft()
.
Cuối cùng, tốt nhất để tránh các từ phủ nhận trong 1 tên. Ví dụ, thay vì1
bool disable_ssl = false;
Nó nên dễ dàng được đọc hơn nếu nói là1
bool use_ssl = true;
Matching Expectations of Users: Phù hợp với mong đợi của người dùng
Một vài cái tên gây hiểu nhầm vì người dùng đã có những dự đoán trước về nghĩa của tên đó, mặc dù bạn có thể có nghĩa khác. Trong trường hợp này, cách tốt nhất là chỉ “chịu thua” và thay đổi tên nó để không gây hiểu nhầm.
Example: get*()
Rất nhiều lập trình viên đã từng quy ước rằng phương thức bắt đầu với get sẽ là “lightweight accessors” (dịch tạm là lấy dữ liệu đơn giản, nhanh và nhẹ ý), cái mà đơn giản trả về 1 phần tử bên trong. Đi ngược lại quy ước này có khả năng đánh lừa người dùng đó.
Đây là ví dụ trong java, cái mà không làm vậy1
2
3
4
5
6
7
8public class StatisticsCollector {
public void addSample(double x) { ... }
public double getMean() {
// Iterate through all samples and return total / num_samples
}
...
}
Trong trường hợp này, khi thực thi getMean
, nó sẽ thực hiện vòng lặp qua các dữ liệu từ trước và tính toán giá trị trung bình. Bước này trả giá rất đắt nếu có rất nhiều dữ liệu. Nhưng 1 lập trình viên không nghĩ ngờ gì có thể gọi getMean()
1 cách bất cẩn, vì nghĩa nó là 1 lời gọi không đắt.
Thay vào đó, phương thức nên được đặt tên như computeMean()
, cái mà sẽ nói lên toán tử tốn kém.
Example: list::size()
Đây là 1 ví dụ trong thư viện chuẩn C++. Đoạn code dưới đây là nguyên nhân gây ra rất nhiều bug khó phát hiện, khiến servers của chúng ta chậm hoặc sập:1
2
3
4
5
6void ShrinkList(list<Node>& list, int max_size) {
while (list.size() > max_size) {
FreeNode(list.back());
list.pop_back();
}
}
Lỗi ở đây là tác giả không biết list.size()
là 1 toán tử O(n) - nó đến qua các node liên kết trong danh sách, thay vì chỉ trả về 1 phép đếm trước, điều này làm cho ShrinkList()
là 1 toán tử O(n2).
Đoạn code “đúng” về mặt kỹ thuật và trên thực tế pass tất cả unit test. Nhưng khi ShrinkList()
được gọi với 1 dánh sách 1 triệu phần tử, nó sẽ tốn 1 giờ để kết thúc.
Có thể bạn đang nghĩ “Đó là lỗi của người gọi - anh ta hoặc cô ta nên phải đọc document của hàm này 1 cách cẩn thận”. Điều đó đúng nhưng trong trường hợp này, thực tế là list.size
không phải là toán tử có thời gian cố định là 1 sự ngạc nhiên. Tất cả các phần khác trong C++ có size là phương thức thời gian cố định.
Nếu size được đặt tên là countSize()
hoặc countElements()
, thì các lỗi tương tự sẽ ít xảy ra hơn. Người viết thư viện chuẩn của C++ có thể muốn đặt tên phương thức là size()
để dùng cho tất cả các phần chưa nó như vector và map. Nhưng vì họ đã làm nnos, lập trình viên dễ dàng mắc lỗi nó là 1 toán tử nhanh, như cách của các containers khác. Rất may, trong thư viện mới nhất của C++, bây giờ thời gian size
đã là O(1)
.
WHO’S THE WIZARD?
A while ago, one of the authors was installing the OpenBSD operating system. During the disk formatting step, a complicated menu appeared, asking for disk parameters. One of the options was to go to “Wizard mode.” He was relieved to find this user-friendly option and selected it. To his dismay, it dropped the installer into a low-level prompt waiting for manual disk formatting commands, with no clear way to get out of it. Evidently “wizard” meant you were the wizard!
Example: Evaluating Multiple Name Candidates
Khi bạn quyết định chọn 1 tên tốt, bạn phải có nhiều thí sinh để cân nhắc.
High-traffic website thường xuyên sử dụng “thí nghiệm” để kiểm tra xem 1 thay đổi trên trang web có cải thiện được kinh doanh hay không? Đây là 1 ví dụ của file cấu hình kiểm soát 1 vài thí nghiệm:
experiment_id: 100
description: “increase font size to 14pt“
traffic_fraction: 5%
…
Mỗi thí nghiệm được định nghĩa bởi 15 cặp thuộc tính/giá trị. Không may, khi bạn định nghĩa 1 thí nghiệm khác tương tự, bạn phải copy và paste lại nhiều dòng:
experiment_id: 101
description: “increase font size to 13pt“
[other lines identical to experimentid 100]
Giả sử bạn muốn fix vấn đề này bằng cách đưa ra 1 cách để có 1 thí nghiệm có thể tái sử dụng các thuộc tính cho các thí nghiệm khác (Nó được gọi là mẫu “prototype inheritance”). Kết quả cuối cùng bạn sẽ chỉ gõ như sau:
experiment_id: 101
the_other_experiment_id_I_want_to_reuse: 100
[change any properties as needed]
Câu hỏi đặt ra là: the_other_experiment_id_I_want_to_reuse
thực sự nên được đặt tên là gì?
Đây là 4 sự cân nhắc cho tên này:
template
reuse
copy
inherit
Bất cứ tên nào trong số này cũng có nghĩa đối với chúng ta. Nhưng chúng ta phải tưởng tượng rằng, tên nào sẽ ra sao đối với với 1 ai đó đọc code và không biết về chức năng này. Hãy phân tích từng tên, nghĩ ra 1 vài cách ai đó có thể giải thích sai nó:
- Xem xét tên
template
experiment_id: 101
template: 100
…
template
có 1 vài vấn đề. Đầu tiên, nó không rõ ràng khi nó đang nói rằng “Tôi là 1 bản mẫu” hoặc “Tôi đang sử dụng mẫu khác”. Cách hiểu thứ 2, một “template” thường là thứ gì đó trừu tượng, cái mà phải được “điền vào” trước khi nó thành cụ thể hóa. Ai đó có thể nghĩ 1 thí nghiệm không phải là 1 thí nghiệm “thực sự”.
- Còn
reuse
thì sao?
experiment_id: 101
reuse: 100
…
reuse
là 1 từ okay nhưng với người viết, ai đó có thể hiểu nó đang nói “Thí nghiệm có thể được tái sử dụng hơn 100 lần”. Thay đổi tên thành reuse_id
sẽ giúp giải quyết điều này. Nhưng nó lại làm bối rối cho người đọc hiểu reuse_id
: 100 nghĩa mà “my id for reuse is 100”.
- Hãy cân nhắc về
copy
:
experiment_id: 101
copy: 100
…
copy
là 1 từ tốt. Nhưng bản thân nó, copy
: 100 dường như đang nói rằng “copy thí nghiệm này 100 lần” hoặc “đây là bản copy thứ 100 của cái mèo gì đó”. Để làm nó rõ ràng khái niệm này đang tham chiếu tới 1 thí nghiệm khác, chúng ta có thể đổi tên là copy_experiment
. Đây có thể là 1 cái tên tốt nhất cho đến thời điểm này.
- Và bây giờ hãy xem xét
inherit
experiment_id: 101
inherit: 100
…
Từ inherit
phổ biến với nhiều lập trình viên và nó được hiểu là sửa đổi thêm sau khi được kế thừa. Với lớp kế thừa, bạn sẽ nhận tất cả các phương thức và thành phần của lớp khác và sau đó định nghĩa lại chúng hoặc thêm mới. Thậm chí trong thế giới thực, khi bạn kế thừa tài sản từ người thân, nó cũng được hiểu là mạn có thể bán chúng hoặc sở hữu những thứ khác nữa.
Nhưng quay trở lại vấn đề, hãy làm cho rõ ràng chúng ta đang kế thừa từ thí nghiệm khác. Chúng ta có thêt cải thiện tên này với cái tên inherit_from
hoặc thậm chí là inherit_from_experiment_id
.
Nhìn chung copy_experiment
và inherit_from_experiment_id
là những cái tên tốt nhất vì chúng mô tả rõ ràng nhất cái mà thực sự đang diễn ra và ít bị hiểu nhầm nhất.
Tổng kết
Những tên tốt nhất chỉ nên có 1 nghĩa để không thể bị hiểu nhầm - người đọc code của bạn sẽ hiểu nó như cách bạn hiểu và không có cách khác. Thiếu may mắn, rất nhiều từ trong tiếng Anh không rõ nghĩa khi chúng ta sử dụng nó trong lập trình như là filter
, length
, và limit
.
Trước khi bạn quyết định 1 cái tên, hãy biện hộ và tưởng tượng rằng tên của bạn có thể bị hiểu nhầm như thế nào? Tên tốt nhất có khả năng chống lại giải thích sai ý nghĩa.
Khi bạn định nghĩa giới hạn trên dưới, max_
và min_
là những tiền tố tốt. Đối với các phạm vi bao gồm, first
và last
là sự lựa chọn. Đối với các phạm vi bao gồm/loại trừ, begin
và end
là tốt nhất vì chúng là quốc ngữ =)).
Khi tên của bạn là 1 boolean, sử dụng những từ như is
và has
để làm rõ nghĩa nó là boolean. Tránh sử dụng các từ có nghĩa phủ định (ví dụ như disable_ssl
)
Coi chừng từ kì vọng của người dùng về những từ nhất định. Ví dụ, người dùng sẽ mong đợi get()
và size()
là các phương thức nhẹ (Beware of users’ expectations about certain words.)
Từ khóa mới: lightweight accessors, lightweight operator => ý nói các toán tử nhẹ và nhanh, không có xử lý logic phức tạp
Tài liệu tham khảo
Chapter 3: Names That Can’t Be Misconstrued
http://yoursite.com/2020/05/24/Chapter-3-Names-That-Can’t-Be-Misconstrued/