Lesson 0.7.5 — Readable JavaScript
0. Metadata
| Field | Value |
|---|---|
| Stage | 0 — JavaScript Language Foundation |
| Module | 0.7 — Error Handling & Code Quality |
| Lesson | 0.7.5 |
| Competency | C01.8 — Error Handling / C13 — Engineering Judgment (Foundation) |
| Depth Target | L2–L3 |
| Prerequisites | Guard Clauses (0.4.4), Functions (0.5.1), Data Structures (0.6.x), Destructuring (0.6.7), Spread (0.6.8), Defensive Programming (0.7.4) |
| Estimated Cognitive Load | Medium |
1. Why This Exists (Vì sao cần học)
Bạn đã biết viết code chạy đúng. Nhưng trong production, code được đọc nhiều hơn code được viết. Một developer đọc code của người khác (và của chính mình 3 tháng trước) nhiều hơn gấp 10 lần viết code mới.
Xét đoạn code:
function d(a) {
let x = 0;
for (let i = 0; i < a.length; i++) {
if (a[i].s === 1) {
if (a[i].v > x) {
x = a[i].v;
}
}
}
return x;
}Vấn đề cốt lõi
Đoạn code trên chạy đúng. Nhưng bạn mất 30 giây để đoán nó làm gì. Tên biến d, a, x, s, v không nói gì về intent. Deep nesting làm bạn phải đếm dấu ngoặc. Mutation x = a[i].v giấu logic trong loop.
Readable JavaScript không phải "làm cho đẹp". Nó là giảm cognitive load cho người đọc — bao gồm chính bạn trong tương lai — để họ hiểu intent trong 3 giây thay vì 3 phút.
Bài này dạy bạn:
- Đặt tên nói về intent, không phải mechanism.
- Giữ function nhỏ, làm một việc.
- Dùng guard clause và early return để tránh deep nesting.
- Kiểm soát mutation —
constưu tiên, tránh sửa input. - Viết explicit — không dùng magic number, không dùng boolean trap.
Đây là bước chuyển từ "Junior viết code máy hiểu" sang "Engineer viết code người hiểu".
2. Prerequisites (Yêu cầu đầu vào)
Trước khi học bài này, bạn cần:
- Biết
const/letvà phân biệt binding với mutation (0.2.2). - Biết guard clause và early return (0.4.4).
- Biết function declaration, expression, arrow function (0.5.1–0.5.3).
- Biết array methods
map,filter,reduce(0.6.3–0.6.4). - Biết destructuring và spread (0.6.7–0.6.8).
- Hiểu defensive programming và validation (0.7.4).
WARNING
Nếu bạn chưa chắc tại sao const ưu tiên hơn let, quay lại 0.2.2. Mutation control là nền tảng của readability.
3. Learning Objectives (Mục tiêu học tập)
Sau bài này, bạn có thể:
- Đặt tên biến và function nói về intent, không phải kiểu dữ liệu.
- Tách function lớn thành các function nhỏ có trách nhiệm rõ ràng.
- Sử dụng guard clause để giảm độ sâu nesting.
- Ưu tiên
constvà tránh mutate argument của function. - Viết code explicit — thay thế magic number và boolean trap bằng tên rõ ràng.
- Refactor một đoạn code "chạy đúng nhưng khó đọc" thành dễ hiểu hơn mà không đổi behavior.
4. Mental Model (Mô hình tư duy)
Mental Model
Readable Code = Giảm câu hỏi của người đọc
Tên rõ ràng
↓
Người đọc không hỏi: "cái này là gì?"
↓
Function nhỏ, một việc
↓
Người đọc không hỏi: "nó còn làm gì nữa?"
↓
Không nested sâu
↓
Người đọc không hỏi: "dấu ngoặc này đóng ở đâu?"
↓
Ít mutation
↓
Người đọc không hỏi: "giá trị này đổi lúc nào?"
Cognitive Load Budget
Mỗi dòng code tốn "năng lượng" để hiểu.
Readable code = tốn ít năng lượng nhất để truyền đạt nhiều intent nhất.Quy tắc vàng:
- Tên là comment tốt nhất.
isActivetốt hơnflag.calculateTaxtốt hơncalc. - Một function = một việc. Nếu tên có chữ "và" (
validateAndSave), tách đôi. - Early return > deep nesting. Mỗi cấp
iflồng nhau tăng cognitive load. const>let>var. Nếu giá trị không đổi, dùngconst. Người đọc không cần theo dõi reassignment.- Explicit > Implicit.
STATUS_PENDINGtốt hơn0.options = { includeTax: true }tốt hơnprocess(data, true).
5. Core Concepts (Các khái niệm cốt lõi)
Essential (Bắt buộc)
| Concept | Ý nghĩa |
|---|---|
| Intent-based naming | Tên mô tả tại sao tồn tại, không phải kiểu gì. userList tốt hơn array. |
| Single Responsibility | Một function làm một việc. Tên function nên mô tả việc đó. |
| Guard clause | if (!valid) return; — giảm nesting bằng cách dừng sớm. |
| Avoid deep nesting | Tối đa 2 cấp if. Nếu hơn, tách function hoặc dùng early return. |
| Mutation control | Dùng const khi có thể. Không sửa argument truyền vào. |
| Explicitness | Không dùng magic number. Không dùng boolean argument không tên (fn(data, true)). |
Supporting (Hỗ trợ)
| Concept | Ý nghĩa |
|---|---|
| Destructuring parameter | function({ name, age }) — tự document shape input. |
| Pure function preference | Output chỉ phụ thuộc input, không side effect. Dễ đọc, dễ test. |
Awareness (Biết tồn tại)
| Concept | Lý do chưa đào sâu |
|---|---|
| Linting (ESLint) | Tự động enforce style. Thuộc Stage 7 (Toolchain). |
| Code review checklist | Quy trình team. Thuộc Stage 9/14. |
| Refactoring patterns | Extract method, rename, v.v. Thuộc Stage 13/14. |
Out of Scope (Không thuộc bài này)
- Design pattern (Module, Factory...). Thuộc Stage 2/12.
- Performance optimization (đôi khi readability trade-off với perf). Thuộc Stage 11.
- TypeScript type annotation cho readability. Thuộc Stage 6.
6. Worked Example (Ví dụ phân tích từng bước)
Bài toán: Refactor function tính tổng giá trị đơn hàng.
function calc(o) {
let t = 0;
for (let i = 0; i < o.length; i++) {
if (o[i].a === 1) {
let p = o[i].p;
if (o[i].q) {
p = p * o[i].q;
}
if (o[i].d) {
p = p - o[i].d;
}
t += p;
}
}
return t;
}function calculateOrderTotal(items) {
const activeItems = items.filter(item => item.status === "active");
return activeItems.reduce((total, item) => {
const itemTotal = calculateItemTotal(item);
return total + itemTotal;
}, 0);
}
function calculateItemTotal(item) {
const basePrice = item.price * (item.quantity || 1);
const discount = item.discount || 0;
return basePrice - discount;
}Walkthrough
Step 1 — Rename
calc→calculateOrderTotal— nói rõ tính gì của cái gì.o→items— đây là danh sách item, không phải object chung chung.t→total— accumulator.a→status,p→price,q→quantity,d→discount— không dùng abbreviation.
Step 2 — Tách logic lọc
if (o[i].a === 1)là lọc item active. Tách thànhfilter. Người đọc thấyactiveItems— rõ ràng hơna === 1(magic number).
Step 3 — Tách function con
- Logic tính giá một item (
price * quantity - discount) thànhcalculateItemTotal. Function mẹ chỉ còn "lọc rồi cộng" — đúng một việc.
Step 4 — Loại bỏ mutation
let t = 0vàt += p→reducevớiconstaccumulator. Không cònlettrong function.let p = ...; p = p * ...→const basePrice,const discount. Không reassign.
Step 5 — Explicit default
o[i].qtruthy check →item.quantity || 1. Rõ ràng rằng missing quantity = 1.o[i].d→item.discount || 0. Rõ ràng rằng missing discount = 0.
Key Insight: Code dài hơn một chút nhưng mỗi dòng trả lời một câu hỏi, không tạo ra câu hỏi mới. Đây là density of intent.
7. Prediction Exercise (Bài tập dự đoán)
Đừng chạy code. Đọc và dự đoán:
- (1) Đoạn nào dễ đọc hơn? Tại sao?
- (2) Có bug ẩn trong đoạn "khó đọc" không?
Câu 1
// Version A
function process(data, flag) {
let r = [];
for (let i = 0; i < data.length; i++) {
if (flag) {
if (data[i].active) {
r.push(data[i].name.toUpperCase());
}
} else {
r.push(data[i].name.toLowerCase());
}
}
return r;
}
// Version B
function formatUserNames(users, { onlyActive, transform }) {
const shouldInclude = onlyActive
? user => user.active
: () => true;
const format = transform === "upper"
? name => name.toUpperCase()
: name => name.toLowerCase();
return users
.filter(shouldInclude)
.map(user => format(user.name));
}Câu 2
// Version A
const r = await fetch('/api');
const d = await r.json();
if (d.s === 200) {
const u = d.data.users;
let a = [];
for (let i = 0; i < u.length; i++) {
if (u[i].role === 1) {
a.push({ n: u[i].name, e: u[i].email });
}
}
return a;
} else {
throw new Error('fail');
}
// Version B
const response = await fetch('/api');
const payload = await response.json();
if (payload.status !== 200) {
throw new Error(`API failed: ${payload.status}`);
}
const users = payload.data?.users || [];
const admins = users
.filter(user => user.role === ROLES.ADMIN)
.map(user => ({ name: user.name, email: user.email }));
return admins;Ghi chú:
awaitthuộc Stage 3 (Async & Concurrency).?.(optional chaining) là ES2020, chưa được dạy ở Stage 0.
Có thể thay bằng sync code và defensive check (payload.data && payload.data.users) || [] ở stage hiện tại.
Nhưng trong production nên dùng await và optional chaining để dễ đọc.
// Current Stage 0
// Version A
const r = fetchSync('/api');
const d = JSON.parse(r);
//...các đoạn code tiếp theo
// Version B
const response = fetchSync('/api'); // Giả lập sync API call cho ví dụ
const payload = JSON.parse(response);
if (payload.status !== 200) {
throw new Error('API failed: ' + payload.status);
}
const users = (payload.data && payload.data.users) || [];
const admins = users
.filter(user => user.role === ROLES.ADMIN)
.map(user => ({ name: user.name, email: user.email }));
return admins;Câu 3 (Transfer — Phân biệt mutation bug)
function addTimestamp(items) {
for (let i = 0; i < items.length; i++) {
items[i].createdAt = Date.now();
}
return items;
}Câu hỏi:
addTimestampcó bug readability hay bug behavior?- Nếu caller viết
const original = [{ name: "A" }]; const result = addTimestamp(original);,originalbị thay đổi không? Tại sao điều này liên quan đến readability? - Viết lại để vừa readable vừa không mutate input.
[Đáp án & Giải thích]
Câu 1: Version B dễ đọc hơn.
- Giải thích: Version A dùng
flagboolean — boolean trap.process(data, true)không nói rõtruelà gì. Version B dùng destructuring parameter với tên rõ ràng:onlyActive,transform. Version A còn deep nesting 3 cấp (for→if flag→if active). Version B flatten bằngfilter+map.
Câu 2: Version B dễ đọc hơn.
- Giải thích: Version A dùng abbreviation
r,d,u,a,n,e,s. Version B dùngresponse,payload,users,admins. Version A có magic number200và1(role). Version B dùngROLES.ADMIN(giả định constant). Version A mutateabằngpush. Version B dùngmaptạo array mới. Version A deep nesting (if→for→if). Version B guard clause (if (status !== 200) throw) rồi happy path ở top level.
Câu 3:
- 1. Cả hai. Behavior: mutate input. Readability: caller không biết function mutate
items— tênaddTimestampkhông nói "tôi sẽ sửa array của bạn". - 2.
originalbị thay đổi vìitems[i].createdAt = ...mutate object trong array. Đây là side effect ẩn. - 3.jsHoặc nếu muốn explicit về mutation:
function addTimestamp(items) { return items.map(item => ({ ...item, createdAt: Date.now() })); }jsfunction addTimestamp(items) { return items.map(item => { const copy = { ...item, createdAt: Date.now() }; return copy; }); }
Transfer Check
Bạn chưa từng gặp đoạn code sau. Dựa vào mental model, hãy trả lời:
function handle(data) {
if (data) {
if (data.type === "user") {
if (data.action === "create") {
createUser(data);
} else if (data.action === "delete") {
deleteUser(data);
}
}
}
}- Đoạn code trên vi phạm nguyên tắc readability nào?
- Refactor để giảm nesting và tăng explicitness. Giữ nguyên behavior.
[Đáp án & Giải thích]
Deep nesting 3 cấp. Implicit skip — nếu
datafalsy hoặc type khác, function làm gì? Không rõ. Magic string"user","create","delete"— nếu dùng nhiều nơi nên là constant.Refactor:
jsfunction handleEvent(event) { if (!event) return; if (event.type !== "user") return; const actions = { create: createUser, delete: deleteUser }; const handler = actions[event.action]; if (handler) { handler(event); } }Hoặc với guard clause:
jsfunction handleEvent(event) { if (!event || event.type !== "user") return; if (event.action === "create") return createUser(event); if (event.action === "delete") return deleteUser(event); }
Bài học: Mỗi cấp if lồng nhau là một "cửa hầm" người đọc phải bò qua. Early return biến code thành "phẳng" — đọc từ trên xuống như một danh sách.
8. Implementation Lab (Bài lab thực hành)
Level 1 — Guided (Có hướng dẫn)
Refactor function sau để dùng tên rõ ràng và guard clause.
function f(a) {
let x = [];
for (let i = 0; i < a.length; i++) {
if (a[i].s === 1) {
x.push(a[i].n);
}
}
return x;
}Gợi ý
f→ tên nói về việc lấy tên user active.a→users.s === 1→status === "active"hoặcisActive.- Dùng
filter+mapthay vìfor+if.
[Đáp án tham khảo]
function getActiveUserNames(users) {
if (!Array.isArray(users)) return [];
return users
.filter(user => user.status === "active")
.map(user => user.name);
}Giải thích:
- Tên
getActiveUserNamesnói rõ intent. - Guard clause
if (!Array.isArray(users))bảo vệ boundary. filter+mapthay thếfor+if— declarative, ít mutation (let xvàpushbiến mất).user.statusthay vìs === 1— không còn magic number.
Level 2 — Partial Scaffold (Khung sẵn)
Hoàn thành refactor function processOrder.
// Before
function p(o) {
let t = 0;
for (let i = 0; i < o.length; i++) {
let s = o[i].p * o[i].q;
if (o[i].t) {
s = s * 1.1;
}
t += s;
}
return t;
}
// After
function calculateOrderTotal(___________) {
if (___________) return 0;
return items.reduce((total, item) => {
const subtotal = item.price * item.quantity;
const _________ = item.taxable ? subtotal * 1.1 : subtotal;
return total + _________;
}, 0);
}[Đáp án tham khảo]
function calculateOrderTotal(items) {
if (!Array.isArray(items)) return 0;
return items.reduce((total, item) => {
const subtotal = item.price * item.quantity;
const lineTotal = item.taxable ? subtotal * 1.1 : subtotal;
return total + lineTotal;
}, 0);
}Giải thích:
p(o)→calculateOrderTotal(items).o[i].p→item.price,o[i].q→item.quantity,o[i].t→item.taxable— boolean có tên rõ ràng thay vìt(không biết là tax hay total hay type).let t = 0; t += s→reducevớiconstaccumulator.- Magic number
1.1vẫn còn nhưng giờ nằm trong contexttaxable— dễ hiểu hơn. Ở production nên tách thànhTAX_RATE = 1.1.
Level 3 — Independent (Tự viết)
Refactor các function sau. Giữ nguyên behavior, tăng readability.
// 1. getAdminNames
function f1(d) {
let r = [];
for (let i = 0; i < d.length; i++) {
if (d[i].r === 1 && d[i].a) {
r.push(d[i].n);
}
}
return r;
}
// 2. createUser
function f2(d) {
let u = {};
u.n = d.n;
u.e = d.e;
if (d.r) {
u.r = d.r;
} else {
u.r = "user";
}
return u;
}
// 3. formatStatus
function f3(s) {
if (s === 0) return "pending";
if (s === 1) return "active";
if (s === 2) return "inactive";
return "unknown";
}
// Sử dụng:
const users = [
{ n: "Alice", r: 1, a: true },
{ n: "Bob", r: 2, a: true },
{ n: "Carol", r: 1, a: false }
];
console.log(getAdminNames(users)); // ["Alice"]
console.log(createUser({ n: "Dave", e: "d@e.com" }));
// { name: "Dave", email: "d@e.com", role: "user" }
console.log(formatStatus(1)); // "active"[Đáp án tham khảo]
function getAdminNames(users) {
if (!Array.isArray(users)) return [];
return users
.filter(user => user.role === ROLES.ADMIN && user.active)
.map(user => user.name);
}
function createUser(data) {
return {
name: data.name,
email: data.email,
role: data.role || "user"
};
}
function formatStatus(statusCode) {
const statusMap = {
0: "pending",
1: "active",
2: "inactive"
};
return statusMap[statusCode] || "unknown";
}
// Constants (nên đặt ở module level)
const ROLES = { ADMIN: 1, USER: 2 };Giải thích:
getAdminNames: Tên rõ ràng, guard clause,filter+map, không mutation.createUser: Object literal thay vì từng dòng gán. Destructuring/shape rõ ràng. Default|| "user".formatStatus: Lookup table thay vì chuỗiif. Dễ thêm trạng thái mới. Không còn magic number rải rác.
9. Edge Cases (Các trường hợp ngoại lệ)
"Refactor" đổi behavior
// Before
function getNames(items) {
const result = [];
for (let i = 0; i < items.length; i++) {
if (items[i].name) {
result.push(items[i].name);
}
}
return result;
}
// After (tưởng là refactor nhưng đổi behavior)
function getNames(items) {
return items.map(item => item.name);
}Tại sao: for version skip falsy name ("", undefined). map version giữ lại undefined. Behavior khác.
Cách nhận biết: Test fail sau refactor. Hoặc result.length khác nhau.
Cách xử lý: Nếu cần giữ behavior: items.map(...).filter(Boolean) hoặc items.filter(item => item.name).map(...). Đảm bảo behavior trước khi "làm đẹp".
Tên quá dài cũng là cognitive load
function getAllActiveUsersFromDatabaseAndFormatTheirNamesToUpperCase(users) {
// ...
}Tại sao: Tên dài thường nghĩa là function làm nhiều việc. Không phải tên dài = tốt.
Cách nhận biết: Tên > 40 ký tự hoặc có nhiều động từ.
Cách xử lý: Tách function. getActiveUsers + formatNamesToUpper.
Over-abstract hóa
const process = compose(
filter(isActive),
map(getName),
map(toUpper)
);Tại sao: Quá nhiều abstraction cho codebase chưa cần. compose là pattern từ functional programming — bạn chưa học ở Stage 0. Junior đọc không hiểu vì cần biết compose hoạt động thế nào (right-to-left hay left-to-right?).
Cách nhận biết: Một dòng code dùng 4 hàm chưa định nghĩa hoặc pattern chưa học.
Cách xử lý: Ở Stage 0, ưu tiên explicit loop hoặc method chain (filter(...).map(...)) hơn functional composition phức tạp. Readable cho audience hiện tại. compose sẽ được học ở Stage 2 (Functional Programming patterns) nếu curriculum có.
const với object vẫn cho phép mutation
function process(users) {
const result = [];
users.forEach(user => {
user.processed = true; // Mutation!
result.push(user);
});
return result;
}Tại sao: const bảo vệ binding, không bảo vệ object. Tên process không nói rõ "tôi sẽ sửa user của bạn".
Cách nhận biết: Side effect ẩn. Caller không biết input bị đổi.
Cách xử lý: Nếu cần thêm field, tạo object mới:
users.map(user => ({ ...user, processed: true }));Hoặc đặt tên rõ ràng: markUsersAsProcessed — tên nói rõ có mutation.
10. Debug Lab (Bài lab gỡ lỗi)
Symptom (Triệu chứng): Code hoạt động đúng nhưng mỗi lần sửa lại gây bug mới.
function handle(data) {
if (data) {
if (data.type) {
if (data.type === "A") {
if (data.value > 10) {
return processA(data);
} else {
return processASmall(data);
}
} else if (data.type === "B") {
return processB(data);
}
}
}
return null;
}Reproduction (Tái hiện lỗi): Thêm type "C" vào logic. Developer thêm else if nhưng nhầm dấu ngoặc.
Evidence (Bằng chứng):
- 4 cấp nesting. Đếm
if/elsemệt mỏi. data.type === "A"vàdata.value > 10— logic phân nhánh phức tạp.- Khi thêm type mới, dễ đặt sai vị trí (ví dụ: sau
return nullhoặc trong nhánh sai).
Hypothesis (Giả thuyết): Deep nesting làm code "giòn" (brittle) — dễ vỡ khi thay đổi. Mỗi cấp if là một trạng thái mental stack mà developer phải giữ trong đầu.
Verification (Xác minh):
// Thử thêm type C:
function handle(data) {
if (data) {
if (data.type) {
if (data.type === "A") {
// ... 4 dòng
} else if (data.type === "B") {
// ...
} else if (data.type === "C") { // Dễ nhầm cấp!
return processC(data);
}
}
}
return null;
}Root Cause (Nguyên nhân gốc rễ): Deep nesting + implicit exit (return null ở cuối cùng, xa nơi quyết định). Không dùng guard clause. Không tách logic phân nhánh.
Fix (Sửa):
function handleEvent(event) {
if (!event || !event.type) return null;
const handlers = {
A: (e) => e.value > 10 ? processA(e) : processASmall(e),
B: processB,
C: processC
};
const handler = handlers[event.type];
return handler ? handler(event) : null;
}Hoặc explicit hơn:
function handleEvent(event) {
if (!event || !event.type) return null;
if (event.type === "A") {
return event.value > 10 ? processA(event) : processASmall(event);
}
if (event.type === "B") return processB(event);
if (event.type === "C") return processC(event);
return null;
}Prevention (Phòng ngừa):
- Hỏi: "Nếu tôi thêm một trường hợp mới, tôi có thể thêm trong 5 giây không?"
- Nếu cần đếm dấu ngoặc để thêm logic, đó là dấu hiệu cần refactor.
- Guard clause + lookup table hoặc early return chain.
11. Design Exercise (Bài tập thiết kế giải pháp)
Bạn cần viết hàm tính phí vận chuyển.
function ship(w, d) {
return w > 10 ? 50 : d === "express" ? 30 : 15;
}function calculateShipping(weight, method) {
const FREE_SHIPPING_THRESHOLD = 10;
const EXPRESS_RATE = 30;
const STANDARD_RATE = 15;
const FREE_SHIPPING_RATE = 50; // Phí cố định cho overweight
if (weight > FREE_SHIPPING_THRESHOLD) {
return FREE_SHIPPING_RATE;
}
if (method === "express") {
return EXPRESS_RATE;
}
return STANDARD_RATE;
}Câu hỏi:
- Option A có lợi gì? Rủi ro gì khi cần sửa logic (ví dụ: thêm method "same-day")?
- Option B dài hơn nhưng lợi gì về maintainability?
- Nếu
weightlàundefined, Option A trả về gì? Option B trả về gì? Điều này nói lên điều gì về defensive?
[Đáp án tham khảo]
Bạn nghĩ:
- Option A: Ngắn gọn, ít dòng. Rủi ro: nested ternary khó đọc. Thêm "same-day" cần sửa cấu trúc ternary, dễ nhầm precedence.
- Option B: Tên constant nói rõ business rule. Thêm method chỉ cần thêm một
if. Dễ đọc, dễ unit test từng branch. - Option A:
undefined > 10làfalse, rồid === "express"→ trả về15(standard). Implied behavior, không rõ là bug hay feature. Option B:undefined > 10làfalse, rơi vàomethod === "express"check. Nếumethodcũng undefined →15. Nhưng ít nhất từng bước explicit, dễ thêm guard clause.
Kết luận: Compact không đồng nghĩa readable. Ternary lồng là "code golf" — thú vị nhưng không maintainable.
12. Production Scenario (Tình huống thực tế)
Context: Bạn nhận code từ developer cũ đã nghỉ việc.
function process(data) {
let a = 0, b = 0;
for (let i = 0; i < data.length; i++) {
if (data[i].t === 1) {
a += data[i].v;
if (data[i].s) {
b += data[i].v * 0.1;
}
} else if (data[i].t === 2) {
a -= data[i].v;
}
}
return { a, b };
}Symptom: PM yêu cầu thêm type 3 (refund). Bạn mất 20 phút đọc code mới dám sửa.
Constraint: Không được đổi output format (API contract). Chỉ được refactor nội bộ.
Câu hỏi:
- Liệt kê 4 vấn đề readability trong đoạn code trên.
- Refactor để thêm type 3 dễ dàng trong 30 giây.
- Tại sao
avàblà tên xấu trong context này?
[Đáp án tham khảo]
Bạn nghĩ:
- 4 vấn đề:
- Tên
a,b,t,v,s— abbreviation không có context. - Magic number
1,2,0.1. - Deep nesting (
for→if→if). - Một function làm cả tính tổng lẫn tính thuế.
- Tên
- Refactor:jsHoặc tốt hơn:
const TRANSACTION_TYPES = { SALE: 1, DISCOUNT: 2, REFUND: 3 }; const TAX_RATE = 0.1; function processTransactions(transactions) { let total = 0; let tax = 0; for (const tx of transactions) { if (tx.type === TRANSACTION_TYPES.SALE) { total += tx.value; if (tx.taxable) { tax += tx.value * TAX_RATE; } } else if (tx.type === TRANSACTION_TYPES.DISCOUNT) { total -= tx.value; } else if (tx.type === TRANSACTION_TYPES.REFUND) { total -= tx.value; // Hoặc logic refund riêng } } return { total, tax }; }jsfunction processTransactions(transactions) { return transactions.reduce((acc, tx) => { const { total, tax } = processTransaction(tx); return { total: acc.total + total, tax: acc.tax + tax }; }, { total: 0, tax: 0 }); } avàbkhông nói gì về ý nghĩa business.totalvàtaxnói rõ đang tính gì.
- 4 vấn đề:
Bài học: Code "chạy được" và code "có thể sửa" là hai chuẩn mực khác nhau. Readable code là code bạn dám sửa mà không sợ phá vỡ.
13. AI-assisted Exercise (Bài tập với AI)
Level B — Challenge
- Tự trả lời trước: Đoạn code sau có vấn đề readability gì?js
const x = users.filter(u => u.a).map(u => u.n); - Hỏi AI: "Refactor đoạn code này cho dễ đọc hơn."
- So sánh câu trả lời AI với nhận định của bạn. AI có đổi
uthànhuserkhông? AI có giải thích tại saou.anên thànhuser.isActivekhông? AI có sử dụng destructuring để tên rõ ràng hơn? - Verify bằng MDN: tìm "Array.prototype.filter" — không có gì đặc biệt, nhưng hãy tự hỏi: "Nếu tôi đọc code này 6 tháng sau, tôi có hiểu
u.alà gì không?"
Gợi ý
AI thường suggest dùng tên đầy đủ nhưng đôi khi giữ lại u vì "ngắn gọn trong callback". Nếu AI không chỉ ra rằng u.a là magic property access và nên thành user.isActive, bạn đã tìm ra điểm mù. AI cũng có thể suggest users.filter(({ isActive }) => isActive) — đây là destructuring trong parameter, rất readable.
[Đáp án tham khảo]
Bạn nghĩ: 3 vấn đề: (1)
uabbreviation. (2)u.amagic property — không biếtalà gì. (3)u.ncũng vậy. Nên thành:jsconst activeUserNames = users .filter(user => user.isActive) .map(user => user.name);Hoặc dùng destructuring:
jsconst activeUserNames = users .filter(({ isActive }) => isActive) .map(({ name }) => name);AI trả lời (typical):
- Rename
utouserfor clarity. - Use destructuring:
users.filter(({ isActive }) => isActive).map(({ name }) => name). - Consider extracting to a named constant:
const activeUserNames = ....
- Rename
So sánh: AI thường đúng về direction nhưng đôi khi không giải thích tại sao điều này quan trọng trong production: abbreviation trong callback tưởng vô hại nhưng khi có 10 callback lồng nhau,
u,i,xtrở thành mê cung.Điểm AI nói sai hoặc quá mơ hồ: "Use clear variable names" — đúng nhưng chung chung. AI hiếm khi nói rõ: "Tên trong callback cũng quan trọng như tên trong function.
u.akhông chỉ khó đọc — nó che giấu business rule (active status) trong implementation detail."Kết luận: Nếu bạn chỉ ra được rằng readable code không chỉ là "tên dài" mà là intent visible at every level, bạn đã hiểu sâu hơn AI. Concept này sẽ quay lại ở Stage 1 (Scope & Closure — tên biến và lexical environment), Stage 8 (React — component naming và props destructuring), và Stage 12 (Architecture — module naming convention).
14. Teach Back (Dạy lại)
Giả sử một junior developer hỏi bạn:
"Em thấy anh đặt tên dài lắm.
calculateOrderTotalthay vìcalc.isUserActivethay vìflag. Code nhìn dài hơn, không mất thời gian gõ à?"
Hãy giải thích trong 2 phút, dùng đúng terminology: cognitive load, intent, maintenance time, read/write ratio.
Mô phỏng
- Bạn nói: Code được đọc nhiều hơn được viết gấp 10 lần. Bạn gõ
calculateOrderTotalmột lần, nhưng team sẽ đọc nó 100 lần trong vòng đời dự án. Thời gian gõ thêm 20 ký tự = 2 giây. Thời gian đoáncalclà tính gì = 30 giây mỗi lần đọc.
Tên dài không phải để "cho đẹp". Nó là để giảm cognitive load — lượng suy nghĩ cần thiết để hiểu code. Khi tôi đọc isUserActive, tôi biết ngay: (1) đây là boolean, (2) nó kiểm tra user, (3) kiểm tra cái gì (active). Khi tôi đọc flag, tôi phải đọc thêm 5 dòng context để biết flag là cái gì.
Về let vs const: Nếu tôi thấy const, tôi không cần theo dõi xem giá trị có đổi không. Nếu tôi thấy let, tôi phải lướt xuống tìm chỗ nó bị ghi đè. Đó cũng là cognitive load.
Lưu ý: Một số team dùng
letcho accumulator trongreducehoặc vòng lặp. Điều đó OK — quan trọng là mục đích rõ ràng.constưu tiên, không phảiconstbắt buộc tuyệt đối.
💡 Tưởng tượng code như một cuốn sách. Tên rõ ràng là tiêu đề chương. Nếu mỗi chương đều tên "Chương 1", "Chương 2", bạn phải đọc hết mới biết nội dung. Tên như "Closure", "Event Loop" cho bạn biết ngay có nên đọc hay không.
Gợi ý đánh giá bản thân
- Đồng nghiệp có hiểu tại sao "thời gian gõ" không quan trọng bằng "thời gian đọc" không?
- Bạn có tránh được việc nói "vì convention" không?
- Nếu đồng nghiệp hỏi: "Vậy khi nào em được phép đặt tên ngắn như
i?" — bạn trả lời được không? (Gợi ý: loop indexilà convention vì scope cực nhỏ và ý nghĩa universal. Nhưngutrongfilter(u => u.a)không phải loop index — nó là business entity.) - Nếu đồng nghiệp viết
if (data) { if (data.type) { ... } }— bạn giải thích được tại sao nên flatten bằng guard clause không?
15. Assessment (Đánh giá)
| Objective (Mục tiêu) | Hình thức đánh giá | Task |
|---|---|---|
| Intent-based naming | Implementation (Thực hành) | Implementation Lab Level 1–3 |
| Single responsibility | Implementation (Thực hành) | Implementation Lab Level 3 |
| Guard clause / reduce nesting | Implementation (Thực hành) | Implementation Lab Level 2–3 |
| Avoid mutation | Prediction (Dự đoán) | Prediction Câu 3 (Transfer) |
| Explicitness (no magic number) | Implementation (Thực hành) | Implementation Lab Level 3 |
| Refactor without behavior change | Debug Lab | Debug Lab Section |
| Compact vs explicit trade-off | Design Exercise | Design Exercise Section |
| Explain readability value | Teach Back | Teach Back Section |
16. Exit Criteria (Tiêu chí qua bài)
- [ ] Có thể đặt tên biến/function nói về intent thay vì kiểu dữ liệu.
- [ ] Có thể tách function lớn thành các function nhỏ có trách nhiệm rõ ràng.
- [ ] Có thể sử dụng guard clause để giảm độ sâu nesting xuống tối đa 2 cấp.
- [ ] Có thể ưu tiên
constvà tránh mutate argument của function. - [ ] Có thể thay thế magic number/boolean trap bằng tên rõ ràng.
- [ ] Có thể refactor một đoạn code "chạy đúng nhưng khó đọc" thành dễ hiểu hơn mà giữ nguyên behavior.
- [ ] Có thể nhận diện khi "quá compact" hoặc "quá abstract" gây hại cho readability.
- [ ] Có thể debug lỗi do refactor đổi behavior (ví dụ:
filter+mapvsfor+if).
17. Spiral Connection (Liên kết xoắn ốc)
Previous (Trước): Defensive Programming (0.7.4) — bạn đã biết validate input và fail fast. Giờ bạn học cách viết code để người khác hiểu được logic defensive đó.
Current (Hiện tại): Readable JavaScript — naming, function size, mutation control, explicitness, guard clause. Code là giao tiếp giữa developer.
Next (Tiếp theo):
- Integration Lab — Stage 0 (Project 0) — Tất cả kỹ năng Stage 0 hội tụ: language foundation, data structures, error handling, và readable code. Bạn sẽ viết một CLI Data Processor với validation, transform, filter, aggregate, và error handling — rồi refactor để readable.
- Stage 1 (Execution Model) — Tên biến, scope, và lexical environment. Tại sao tên biến quan trọng trong closure? Tại sao
constvsletảnh hưởng mental model về rebinding?- Stage 2 (Object Model) — Method naming, prototype chain, và
this. Readable OOP patterns.- Stage 6 (TypeScript) — Type annotation là một lớp readable thêm vào:
function fn(user: User)tự document hơnfunction fn(user).- Stage 8 (React) — Component naming, props destructuring, custom hooks naming convention.
useActiveUserstốt hơnuseData.- Stage 12/14 (Architecture) — Code review, RFC, ADR. Readable code là prerequisite cho code review hiệu quả và technical decision.