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

Bài 5 · Cơ bản · 16 phút

Định dạng & cấu trúc

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

Định dạng dọc (vertical): khái niệm liên quan đứng gần, hàm gọi gần nhau; mật độ & khoảng trống; độ dài dòng; để máy lo bằng Prettier + ESLint + EditorConfig.

Khi lần đầu mở một file lạ, não bạn không đọc từng dòng - nó quét toàn bộ cấu trúc trước: file này dài bao nhiêu, được chia thành những khối nào, khối nào liên quan nhau. Định dạng nhất quán là tín hiệu giúp não làm việc đó nhanh và chính xác.

Điều quan trọng nhất không phải là bạn chọn 2 space hay 4 space - mà là cả đội nhất quán. Code không nhất quán bắt người đọc xử lý thêm một lớp "tiếng ồn" mỗi lần mắt lướt qua.

  • Định dạng là tín hiệu cấu trúc - giúp não quét nhanh trước khi đọc chi tiết.
  • Nhất quán quan trọng hơn đúng: quy ước sai mà nhất quán còn tốt hơn quy ước đúng mà lộn xộn.
  • Đừng tranh cãi về format trong review - để máy quyết định, người review tập trung vào logic.

Báo giấy và bài blog đều có cùng cấu trúc: tổng quát ở trên, chi tiết ở dưới. Đọc tiêu đề và đoạn mở đầu là nắm được 80% - muốn biết sâu hơn thì đọc tiếp. File code tốt cũng vậy: import và khai báo kiểu ở trên cùng, hàm điều phối (orchestrator) kế tiếp, hàm chi tiết ở dưới.

Định dạng dọc: trước và sau

// TRUOC: lon xon - ham chi tiet tren, ham goi no o duoi
function formatName(first: string, last: string): string {
  return first.trim() + ' ' + last.trim();
}

const MAX_RETRIES = 3;

function processUser(user: User): string {
  // ... goi formatName, validateEmail, buildGreeting
  return buildGreeting(validateEmail(user.email), formatName(user.first, user.last));
}

function validateEmail(email: string): boolean {
  return email.includes('@');
}

function buildGreeting(valid: boolean, name: string): string {
  return valid ? 'Chao ' + name : 'Email khong hop le';
}

// ---

// SAU: tong quat tren, chi tiet duoi
// Nguoi doc thay processUser truoc (buc tranh lon), roi di xuong neu can
const MAX_RETRIES = 3;

function processUser(user: User): string {
  return buildGreeting(validateEmail(user.email), formatName(user.first, user.last));
}

function validateEmail(email: string): boolean {
  return email.includes('@');
}

function buildGreeting(valid: boolean, name: string): string {
  return valid ? 'Chao ' + name : 'Email khong hop le';
}

function formatName(first: string, last: string): string {
  return first.trim() + ' ' + last.trim();
}
  • File đọc như bài báo: tổng quát (export/orchestrator) ở trên, chi tiết ở dưới.
  • Hàm gọi đứng GẦN hàm bị gọi - không để người đọc nhảy khắp file để theo luồng.
  • Khai báo biến gần chỗ dùng - không khai báo hàng loạt ở đầu hàm rồi dùng 50 dòng sau.
  • Dòng trống ngăn cách các "ý" - như đoạn văn; quá nhiều hoặc quá ít đều gây khó đọc.

Định dạng ngang nói về những gì xảy ra trong một dòng: khoảng trắng quanh toán tử, độ dài dòng, và thụt lề thể hiện cấu trúc lồng nhau.

Khoảng trắng ngang - dày đặc vs. có hơi thở

// DAY DAC - kho doc
const result=a+b*c;
if(x>0&&y<10){doSomething(x,y,z);}
const obj={name:'meo',age:2,active:true};

// CO HOI THO - mat di theo cau truc
const result = a + b * c;
if (x > 0 && y < 10) {
  doSomething(x, y, z);
}
const obj = { name: 'meo', age: 2, active: true };

Thụt lề thể hiện cấu trúc - không dùng thụt lề để 'căn chỉnh đẹp'

// TRANH: can chinh theo chieu ngang (fragile - doi ten bien la vo)
const firstName  = user.firstName;
const lastName   = user.lastName;
const emailAddr  = user.email;

// NEN: thut deu, de doc, Prettier tu lo
const firstName = user.firstName;
const lastName = user.lastName;
const emailAddr = user.email;
  • Khoảng trắng quanh toán tử và sau dấu phẩy giúp mắt tách được các phần.
  • Thụt lề thể hiện cấu trúc lồng nhau - không dùng để "căn chỉnh đẹp" theo chiều ngang.
  • Giới hạn độ dài dòng (80-120 ký tự) tránh scroll ngang, dễ diff hơn khi review.

Tranh cãi về format trong code review là lãng phí. Mỗi lập trình viên có sở thích cá nhân và không ai sai hoàn toàn - nhưng khi làm việc nhóm, quy ước đội thắng sở thích cá nhân. Giải pháp: để máy quyết định và thực thi, người review tập trung vào logic và thiết kế.

.prettierrc - cấu hình đơn giản cho dự án TypeScript

{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "printWidth": 100,
  "trailingComma": "es5",
  "arrowParens": "always"
}

.editorconfig - áp dụng cho mọi ngôn ngữ trong repo

root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false

Bộ ba công cụ - phân công rõ ràng

Prettier: format code (dấu cách, dấu phẩy, dấu ngoặc) - không phán xét chất lượng logic. ESLint: phát hiện vấn đề chất lượng (biến không dùng, code có thể lỗi, anti-pattern). EditorConfig: đảm bảo tất cả editor dùng cùng charset và line ending. Ba thứ bổ trợ nhau, không thay nhau.

Tích hợp vào package.json - format-on-save và pre-commit

// package.json (phan scripts va lint-staged)
{
  "scripts": {
    "format": "prettier --write .",
    "lint": "eslint . --ext .ts,.tsx",
    "check": "tsc --noEmit"
  },
  "lint-staged": {
    "*.{ts,tsx,js}": ["prettier --write", "eslint --fix"],
    "*.{json,md,yml}": ["prettier --write"]
  }
}
  • Prettier: format tự động, không tranh cãi về dấu phẩy hay ngoặc nữa.
  • ESLint: phát hiện vấn đề chất lượng - để Prettier lo format, ESLint lo logic.
  • EditorConfig: cùng charset/line-ending cho mọi người dù dùng editor nào.
  • Pre-commit hook (lint-staged): format và lint tự động trước mỗi commit - không ai quên.

Khi format được tự động hoá, code review không còn "comment về dấu phẩy". Thay vào đó, mọi người tập trung vào: thiết kế có đúng không, logic có lỗi không, tên có rõ không. Đó là loại review có giá trị.

Codebase với format nhất quán cũng dễ dùng git hơn: diff chỉ hiện thay đổi logic thật, không bị ô nhiễm bởi thay đổi khoảng trắng của người này sang người khác.

Bài tiếp theo: Object vs Data

Tên rõ, comment đúng chỗ, format nhất quán - đây là nền tảng code sạch ở cấp độ dòng và hàm. Bài tiếp theo nâng lên cấp độ thiết kế: Object và Data structure - hai cách tổ chức dữ liệu khác nhau về bản chất, mỗi cách phù hợp với một loại bài toán. Hiểu sự khác biệt giúp bạn chọn đúng công cụ.

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

Câu trả lời đúng: quy ước đội quyết định, Prettier/EditorConfig thực thi, và không ai tranh cãi nữa. Trong hầu hết dự án TypeScript/JavaScript hiện đại: 2 space là phổ biến nhất (Prettier mặc định). Tab có lợi cho khả năng tiếp cận (accessibility) - người dùng có thể tuỳ chỉnh độ rộng. Nhưng chọn một rồi dùng nhất quán quan trọng hơn là chọn đúng.

Dùng eslint-config-prettier để tắt các rule ESLint liên quan đến format (chúng xung đột với Prettier). ESLint lo về chất lượng code (biến chưa dùng, lỗi logic); Prettier lo về format. Phân công rõ ràng, không dẫm chân nhau. Thứ tự: ESLint sửa vấn đề chất lượng, Prettier format sau cùng.

80 ký tự là tiêu chuẩn lịch sử (từ màn hình terminal); 100-120 phổ biến hơn với màn hình hiện đại. Prettier mặc định 80. Quan trọng hơn con số cụ thể là nhất quán: nếu đội chọn 100, thì tất cả đều dùng 100. Dòng quá dài thường là dấu hiệu cần tách hàm hoặc biến trung gian.

Với file TypeScript thông thường (<2000 dòng) thì không đáng kể - Prettier rất nhanh. Nếu cảm thấy chậm, bật formatOnSaveTimeout trong VS Code (mặc định 750ms). Thay thế khác: chạy Prettier qua pre-commit hook (lint-staged) - format tự động trước mỗi commit mà không ảnh hưởng trải nghiệm gõ phím.

Nên có cả hai. EditorConfig hoạt động ở tầng editor (áp dụng cho mọi ngôn ngữ, kể cả file mà Prettier không xử lý: .sh, .yml, .md); Prettier hoạt động ở tầng tool (format code kỹ hơn). EditorConfig đảm bảo ngay cả editor không chạy Prettier cũng dùng đúng charset và line ending.

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

Nguyên tắc "file đọc như bài báo" trong định dạng dọc nghĩa là gì?

  1. 1

    Audit file dài nhất

    Tìm file TypeScript/JavaScript dài nhất trong dự án của mèo con. Đọc từ trên xuống: có thể đọc như "bài báo" (tổng quát trên, chi tiết dưới) không? Liệt kê 2-3 chỗ vi phạm nguyên tắc định dạng dọc.

    Hoàn thành khi: Chỉ ra cụ thể: chỗ nào khai báo biến xa chỗ dùng, chỗ nào hàm gọi nhau đứng cách xa, chỗ nào thiếu/thừa dòng trống.

  2. 2

    Cài đặt Prettier

    Thêm Prettier vào một dự án TypeScript (mới hoặc có sẵn): cài package, tạo .prettierrc, bật format-on-save trong VS Code (hoặc editor mèo con dùng).

    Hoàn thành khi: Lưu một file TypeScript và nó tự format; npx prettier --check . báo "All matched files use Prettier code style!"

  3. 3

    Tạo .editorconfig

    Tạo file .editorconfig cho dự án của mèo con với ít nhất: root=true, charset UTF-8, indent style/size khớp với Prettier, end_of_line, trim_trailing_whitespace, insert_final_newline.

    Hoàn thành khi: File .editorconfig commit được, editor áp dụng đúng (kiểm bằng cách mở file mới và xem tab/space mặc định).

  4. 4

    Sắp xếp lại một file lộn xộn

    Lấy một file TypeScript có hàm/khai báo sắp xếp lung tung (hoặc tự tạo một file như vậy). Sắp xếp lại theo nguyên tắc: import → types/interfaces → constants → hàm cấp cao (gọi người khác) → hàm chi tiết (bị gọi). Không đổi logic.

    Hoàn thành khi: Diff chỉ là di chuyển, không thay đổi hành vi; file đọc theo thứ tự tự nhiên từ trên xuống.

  5. 5

    Thêm ESLint + phân công rõ

    Cài ESLint vào dự án TypeScript có Prettier. Thêm eslint-config-prettier để tránh xung đột. Chạy eslint . và sửa ít nhất 1 warning/error thật (không phải chỉ format).

    Hoàn thành khi: npx eslint . chạy sạch (0 error); Prettier và ESLint không báo xung đột nhau; phân công rõ: ESLint = chất lượng, Prettier = format.

  6. 6

    Thêm pre-commit hook

    Dùng lint-staged + husky (hoặc simple-git-hooks) để tự động chạy Prettier và ESLint trước mỗi commit. Commit một file chưa format và xem hook tự sửa hoặc chặn.

    Hoàn thành khi: Thử commit một file có lỗi lint - hook chặn commit và báo lỗi; commit file sạch - hook cho qua.