← Clean Code: viết code dễ đọc

Bài 2 · Cơ bản · 20 phút

Đặt tên có ý nghĩa

Biên soạn bởi Nguyễn Anh Tuấn

Đặt tên rõ ý định (intention-revealing), phát âm & tìm được (searchable); tránh viết tắt/tên gây nhiễu & mã hoá kiểu vào tên; một khái niệm một từ.

Mỗi lần thêm comment để giải thích một cái tên mơ hồ, bạn đang trả giá gấp đôi: viết tên rồi còn phải viết thêm comment. Tệ hơn, comment có thể lỗi thời mà tên vẫn còn đó. Tên rõ ý định là comment rẻ nhất - nó không bao giờ lỗi thời vì nó chính là code.

Tên mơ hồ vs tên rõ ý định

// Truoc: phai doc than ham moi hieu d, u[], r la gi
function d(u: User[]): User[] {
  const r = [];
  for (let i = 0; i < u.length; i++) {
    if (u[i].a && u[i].t <= 30) r.push(u[i]);
  }
  return r;
}

// Sau: ten ke ro cau chuyen - khong can comment
function activeUsersSeenRecently(users: User[]): User[] {
  return users.filter((u) => u.isActive && u.daysSinceLogin <= 30);
}

Hai đoạn code làm đúng một việc. Phiên bản sau không cần một dòng comment nào - tên hàm, tên tham số, và tên trường nói hết rồi. Đó chính là tối ưu cho người đọcbài 1 đã đặt nền.

  • Tên rõ ý định xoá nhu cầu comment giải thích - code tự kể câu chuyện.
  • Tên mơ hồ bắt người đọc giải mã mỗi lần đọc; nhân với hàng trăm lần đọc là rất tốn.
  • Comment giải thích tên dễ lỗi thời; tên tốt thì không.

Tên xấu không chỉ là tên viết tắt. Có ba loại bẫy phổ biến hơn và nguy hiểm hơn vì chúng trông có vẻ "ổn":

1. Disinformation - thông tin sai lệch. Tên gợi ý sai kiểu hoặc sai hành vi. Ví dụ: gọi một mảng là accountList khi nó không phải List (mà là mảng thường). Người đọc sẽ tìm phương thức .add() của List rồi bối rối.

Disinformation: tên hứa hẹn kiểu sai

// Sai: accountList goi y la List nhung la mang thong thuong
const accountList: Account[] = getAccounts();
accountList.add(newAccount); // loi: Array khong co .add()

// Dung: ten khop voi kieu thuc su
const accounts: Account[] = getAccounts();
const accountSet = new Set<Account>();  // neu la Set moi dat ten Set

2. Tên gần giống nhau. Hai tên chỉ khác nhau một vài ký tự cuối gây nhầm lẫn khi đọc nhanh: XYZControllerForEfficientHandlingOfStrings vs XYZControllerForEfficientStorageOfStrings. Cũng vậy với getUserInfo, getUserData, getUserRecord - ba tên cho một loại thao tác, không rõ khác nhau chỗ nào.

3. Mã hoá kiểu vào tên (Hungarian notation, tiền tố/hậu tố kiểu). TypeScript đã có hệ thống kiểu tĩnh - IDE biết kiểu khi hover, trình kiểm tra bắt lỗi tự động. Tiền tố như strName, bIsValid, m_userId vừa thừa vừa lỗi thời ngay khi bạn đổi kiểu mà quên đổi tiền tố.

Mã hoá kiểu vào tên: thừa và dễ lỗi thời

// Truoc: tien to kieu - du thua khi da co TypeScript
const strUserName: string = 'Meo Con';
const bIsActive: boolean = true;
const nRetryCount: number = 3;

// Sau: kieu la cong viec cua TypeScript, ten chi can ro y dinh
const userName: string = 'Meo Con';
const isActive: boolean = true;
const retryCount: number = 3;
  • Disinformation: tên hứa hẹn kiểu hoặc hành vi sai với thực tế.
  • Tên gần giống nhau gây nhầm lẫn khi đọc lướt - dùng một từ rõ ràng, khác hẳn.
  • Hungarian notation thừa với TypeScript; tiền tố kiểu còn có thể lỗi thời khi refactor.

Hai tiêu chí này nghe nhỏ nhặt nhưng ảnh hưởng hàng ngày: bạn đọc to tên khi thảo luận với đồng đội, và bạn tìm tên khi debug hoặc refactor.

Tên khó phát âm vs tên phát âm được

// Kho phat am: genymdhms la gi? (gen + ym + dhms?)
interface Record {
  genymdhms: Date;   // generation year-month-day-hour-minute-second
  modymdhms: Date;
  pszqint: number;
}

// Ro rang: phat am duoc, doc la hieu ngay
interface Record {
  generationTimestamp: Date;
  modificationTimestamp: Date;
  recordVersion: number;
}

Tên "tìm được" (searchable) quan trọng không kém. Magic number như 7 hay 86400 xuất hiện ở khắp nơi trong codebase - grep ra hàng trăm kết quả không liên quan. Hằng số đặt tên thì grep ngay được đúng chỗ:

Magic number vs hằng số có tên

// Truoc: 7 la gi? Max session? Max retry? Days in week?
if (retries > 7) abort();
const seconds = duration * 86400;

// Sau: ten noi ro y dinh; grep MAX_RETRIES tim duoc ngay
const MAX_RETRIES = 7;
const SECONDS_PER_DAY = 86_400;

if (retries > MAX_RETRIES) abort();
const seconds = duration * SECONDS_PER_DAY;

Kinh nghiệm thực tế

Nếu tên chỉ có 1-2 ký tự (trừ biến vòng lặp i/j/k trong thân vòng lặp ngắn), đó là dấu hiệu cần đặt lại. Nếu grep ra quá nhiều kết quả không liên quan, tên chưa đủ "searchable".
  • Tên phát âm được giúp thảo luận với đồng đội - không ai muốn đánh vần "genymdhms".
  • Magic number không grep được; hằng số có tên thì grep ra đúng chỗ ngay lập tức.
  • Tên 1-2 ký tự (ngoài biến vòng lặp) và số kỳ diệu là hai "mùi" dễ phát hiện nhất.

Codebase nhất quán dễ đọc hơn codebase "tự do". Một số quy ước giới lập trình đồng thuận rộng và nên áp dụng:

Quy ước đặt tên TypeScript

// Lop/type/interface: danh tu, mo ta MOT thu
class UserAccount { }      // tot
class ProcessUserData { }  // xau - nghe nhu ham

// Ham: bat dau bang dong tu, mo ta HANH DONG
function getUser(id: string): User { }     // tot
function deleteExpiredSessions(): void { } // tot
function userData(): User { }              // xau - danh tu cho ham?

// Boolean: bat dau bang is/has/can/should
const isActive: boolean = true;
const hasPermission: boolean = false;
const canRetry: boolean = retries < MAX_RETRIES;

Quan trọng hơn tất cả là một khái niệm một từ: chọn get hay fetch hay retrieve - rồi dùng nhất quán cho cùng loại thao tác. Người đọc không nên đoán fetchUser với getAccount khác nhau gì (hay thực ra giống nhau mà chỉ khác tên?).

Không nhất quán vs nhất quán một từ

// Khong nhat quan: fetch/get/retrieve dung lan lon
async function fetchUser(id: string): Promise<User> { }
async function getAccount(id: string): Promise<Account> { }
async function retrieveOrder(id: string): Promise<Order> { }

// Nhat quan: tat ca dung get cho thao tac lay du lieu
async function getUser(id: string): Promise<User> { }
async function getAccount(id: string): Promise<Account> { }
async function getOrder(id: string): Promise<Order> { }
  • Lớp/type = danh từ mô tả một thứ; hàm = động từ mô tả hành động.
  • Boolean nên bắt đầu bằng is/has/can/should để đọc như câu hỏi có/không.
  • Một khái niệm một từ: chọn fetch hay get hay retrieve rồi dùng nhất quán cả dự án.

Đừng chơi chữ với tên. Nếu đã có add(a, b) trả tổng hai số, đừng đặt một hàm khác là add(item) để thêm item vào list - hai hành động hoàn toàn khác nhau mà dùng cùng một từ. Người đọc sẽ bị bất ngờ. Dùng insert hay append cho hành động thêm vào collection.

Một bẫy cuối cùng: thêm hậu tố kiểu dư thừa vì TypeScript đã giải quyết rồi. IUserService (tiền tố I cho interface), UserServiceImpl (hậu tố Impl), AbstractBase - những tên này nói về cấu trúc code, không nói về ý định nghiệp vụ. Khi codebase chỉ có một implementation, tên IUserService còn tệ hơn: nó bắt người đọc nhớ một quy ước trống rỗng.

Hậu tố thừa vs tên rõ ý định

// Thua: tien to/hau to chi noi ve cau truc, khong noi ve y dinh
interface IUserRepository { }    // I thua neu chi co 1 implementation
class UserRepositoryImpl { }     // Impl thua - ai goi lop nay cu the?

// Tot: ten noi ve VAI TRO; TypeScript biet day la interface hay class
interface UserRepository {
  getById(id: string): Promise<User>;
  save(user: User): Promise<void>;
}
class PostgresUserRepository implements UserRepository { }
class InMemoryUserRepository implements UserRepository { } // de test

Bài tiếp theo: Hàm nhỏ, làm một việc

Tên tốt là nền tảng. Bước tiếp theo là hàm nhỏ làm một việc - hàm có tên rõ mà làm nhiều việc vẫn là code khó đọc. Hai bài cộng lại cho bạn công cụ viết code tự giải thích.

Câu hỏi thường gặp

Không. Tên biến/hàm chỉ tồn tại trong lúc biên dịch và phát triển; runtime không biết tên là gì. Chi phí duy nhất là gõ lâu hơn vài giây - đổi lại là người đọc (kể cả bạn sau 3 tháng) tiết kiệm nhiều phút mỗi lần.

Trong team quen nhau thì ngắn hạn có thể ổn. Vấn đề đến khi người mới vào, bạn đọc lại sau 6 tháng, hoặc tìm kiếm trong IDE. Viết tắt riêng của team còn rủi ro hơn: acc là accumulator hay account hay accuracy? Tên đầy đủ luôn an toàn hơn.

i, j, k cho vòng lặp ngắn (≤5 dòng thân vòng lặp) là quy ước rộng rãi và mọi người hiểu. Vấn đề khi thân vòng lặp dài, lồng nhiều tầng, hoặc biến được dùng ngoài vòng lặp - lúc đó nên đặt tên thật. Quy tắc: nếu cần cuộn để thấy hết thân vòng lặp thì đặt tên thật.

Hungarian notation sinh ra từ thời C, khi IDE không biết kiểu là gì và lập trình viên cần viết szName (zero-terminated string) để nhớ. TypeScript đã có hệ thống kiểu tĩnh - IDE hiển thị kiểu khi hover, trình kiểm tra kiểu bắt lỗi tự động. Thêm tiền tố kiểu vào tên vừa thừa vừa gây nhiễu, và còn lỗi thời ngay khi bạn đổi kiểu mà quên đổi tiền tố.

Có rủi ro nếu sửa ồ ạt không có test đỡ lưng. Áp dụng quy tắc Boy Scout từ bài 1: trong file bạn đang chạm, chọn một từ và dùng nhất quán trong file đó; đừng đổi cả codebase một lần. Dần dần toàn bộ codebase sẽ hội tụ về một quy ước mà không có ngày "đóng băng tính năng để rename".

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Gọi một mảng Account[]accountList vi phạm nguyên tắc nào về đặt tên?

  1. 1

    Săn tên bí ẩn

    Mở một file trong dự án của mèo con (hoặc mã nguồn mở), tìm 3 tên biến hoặc hàm mà bạn phải đọc thân hàm mới đoán được ý nghĩa. Ghi lại tên cũ và đề xuất tên mới.

    Hoàn thành khi: Ba cặp (tên cũ → tên mới đề xuất) kèm một câu giải thích vì sao tên mới rõ hơn.

  2. 2

    Tìm magic number

    Trong cùng file đó, tìm ít nhất 2 con số hoặc chuỗi "kỳ diệu" (magic number/string) xuất hiện trực tiếp trong code. Đặt tên hằng số có ý nghĩa cho chúng.

    Hoàn thành khi: Hai hằng số mới với tên rõ ý định; code dùng hằng số thay vì con số thô.

  3. 3

    Kiểm tra "searchable"

    Chọn một biến tên ngắn (1-2 ký tự hoặc viết tắt) trong dự án. Thử tìm kiếm (Ctrl+F / grep) tên đó - đếm xem có bao nhiêu kết quả không liên quan xuất hiện.

    Hoàn thành khi: Một con số "kết quả nhiễu" và một câu nhận xét về khả năng tìm được của tên đó.

  4. 4

    Nhất quán một từ

    Grep toàn bộ dự án tìm các hàm dùng lẫn lộn get/fetch/retrieve (hoặc create/make/build) cho cùng loại thao tác. Chọn một từ và viết ra quy ước cho team.

    Hoàn thành khi: Một danh sách hàm không nhất quán + quyết định rõ: dùng từ nào cho loại thao tác nào.

  5. 5

    Xoá tiền tố kiểu

    Tìm trong codebase (hoặc viết một đoạn mẫu) có tên dùng Hungarian notation hoặc hậu tố kiểu (strName, numCount, bIsValid, m_userId). Viết lại chỉ với tên thuần và kiểu TypeScript.

    Hoàn thành khi: Đoạn code trước/sau: tên cũ có tiền tố/hậu tố → tên mới chỉ có tên + kiểu TS; chức năng không đổi.

  6. 6

    Phát âm thử

    Đọc to (hoặc trong đầu) tên 5 biến/hàm trong dự án của mèo con. Đánh dấu tên nào bạn vấp hoặc không biết đọc thế nào. Đổi tên chúng thành phát âm được.

    Hoàn thành khi: Ít nhất 2 tên đã được đổi thành phát âm được và vẫn rõ ý nghĩa.