Skip to content

Lesson 0.7.5 — Readable JavaScript ​

Bài 0.7.5 — Readable JavaScript
Naming, Function size, Early return & Explicitness • 38 phút
0:00 / 0:00

0. Metadata ​

FieldValue
Stage0 — JavaScript Language Foundation
Module0.7 — Error Handling & Code Quality
Lesson0.7.5
CompetencyC01.8 — Error Handling / C13 — Engineering Judgment (Foundation)
Depth TargetL2–L3
PrerequisitesGuard 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 LoadMedium

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:

js
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/let và 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ể:

  1. Đặt tên biến và function nói về intent, không phải kiểu dữ liệu.
  2. Tách function lớn thành các function nhỏ có trách nhiệm rõ ràng.
  3. Sử dụng guard clause để giảm độ sâu nesting.
  4. Ưu tiên const và tránh mutate argument của function.
  5. Viết code explicit — thay thế magic number và boolean trap bằng tên rõ ràng.
  6. 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. isActive tốt hơn flag. calculateTax tốt hơn calc.
  • 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 if lồng nhau tăng cognitive load.
  • const > let > var. Nếu giá trị không đổi, dùng const. Người đọc không cần theo dõi reassignment.
  • Explicit > Implicit. STATUS_PENDING tốt hơn 0. options = { includeTax: true } tốt hơn process(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 namingTên mô tả tại sao tồn tại, không phải kiểu gì. userList tốt hơn array.
Single ResponsibilityMột function làm một việc. Tên function nên mô tả việc đó.
Guard clauseif (!valid) return; — giảm nesting bằng cách dừng sớm.
Avoid deep nestingTối đa 2 cấp if. Nếu hơn, tách function hoặc dùng early return.
Mutation controlDùng const khi có thể. Không sửa argument truyền vào.
ExplicitnessKhô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 parameterfunction({ name, age }) — tự document shape input.
Pure function preferenceOutput chỉ phụ thuộc input, không side effect. Dễ đọc, dễ test.

Awareness (Biết tồn tại) ​

ConceptLý do chưa đào sâu
Linting (ESLint)Tự động enforce style. Thuộc Stage 7 (Toolchain).
Code review checklistQuy trình team. Thuộc Stage 9/14.
Refactoring patternsExtract 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.

js
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;
}
js
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ành filter. Người đọc thấy activeItems — rõ ràng hơn a === 1 (magic number).

Step 3 — Tách function con

  • Logic tính giá một item (price * quantity - discount) thành calculateItemTotal. Function mẹ chỉ còn "lọc rồi cộng" — đúng một việc.

Step 4 — Loại bỏ mutation

  • let t = 0 và t += p → reduce với const accumulator. Không còn let trong function.
  • let p = ...; p = p * ... → const basePrice, const discount. Không reassign.

Step 5 — Explicit default

  • o[i].q truthy 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 ​

js
// 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 ​

js
// 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ú: await thuộ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.

js
// 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) ​

js
function addTimestamp(items) {
  for (let i = 0; i < items.length; i++) {
    items[i].createdAt = Date.now();
  }
  return items;
}

Câu hỏi:

  1. addTimestamp có bug readability hay bug behavior?
  2. Nếu caller viết const original = [{ name: "A" }]; const result = addTimestamp(original);, original bị thay đổi không? Tại sao điều này liên quan đến readability?
  3. 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 flag boolean — boolean trap. process(data, true) không nói rõ true là 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ằng filter + 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ùng response, payload, users, admins. Version A có magic number 200 và 1 (role). Version B dùng ROLES.ADMIN (giả định constant). Version A mutate a bằng push. Version B dùng map tạ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ên addTimestamp không nói "tôi sẽ sửa array của bạn".
  • 2. original bị thay đổi vì items[i].createdAt = ... mutate object trong array. Đây là side effect ẩn.
  • 3.
    js
    function addTimestamp(items) {
      return items.map(item => ({
        ...item,
        createdAt: Date.now()
      }));
    }
    Hoặc nếu muốn explicit về mutation:
    js
    function 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:

js
function handle(data) {
  if (data) {
    if (data.type === "user") {
      if (data.action === "create") {
        createUser(data);
      } else if (data.action === "delete") {
        deleteUser(data);
      }
    }
  }
}
  1. Đoạn code trên vi phạm nguyên tắc readability nào?
  2. Refactor để giảm nesting và tăng explicitness. Giữ nguyên behavior.
[Đáp án & Giải thích]
  1. Deep nesting 3 cấp. Implicit skip — nếu data falsy 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.

  2. Refactor:

    js
    function 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:

    js
    function 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.

js
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ặc isActive.
  • Dùng filter + map thay vì for + if.
[Đáp án tham khảo]
js
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 getActiveUserNames nói rõ intent.
  • Guard clause if (!Array.isArray(users)) bảo vệ boundary.
  • filter + map thay thế for + if — declarative, ít mutation (let x và push biến mất).
  • user.status thay vì s === 1 — không còn magic number.

Level 2 — Partial Scaffold (Khung sẵn) ​

Hoàn thành refactor function processOrder.

js
// 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]
js
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 → reduce với const accumulator.
  • Magic number 1.1 vẫn còn nhưng giờ nằm trong context taxable — dễ hiểu hơn. Ở production nên tách thành TAX_RATE = 1.1.

Level 3 — Independent (Tự viết) ​

Refactor các function sau. Giữ nguyên behavior, tăng readability.

js
// 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]
js
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ỗi if. 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

js
// 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

js
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

js
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

js
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:

js
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.

js
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/else mệ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 null hoặ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):

js
// 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):

js
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:

js
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.

js
function ship(w, d) {
  return w > 10 ? 50 : d === "express" ? 30 : 15;
}
js
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:

  1. Option A có lợi gì? Rủi ro gì khi cần sửa logic (ví dụ: thêm method "same-day")?
  2. Option B dài hơn nhưng lợi gì về maintainability?
  3. Nếu weight là 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ĩ:

    1. 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.
    2. 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.
    3. Option A: undefined > 10 là false, rồi d === "express" → trả về 15 (standard). Implied behavior, không rõ là bug hay feature. Option B: undefined > 10 là false, rơi vào method === "express" check. Nếu method cũ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.

js
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:

  1. Liệt kê 4 vấn đề readability trong đoạn code trên.
  2. Refactor để thêm type 3 dễ dàng trong 30 giây.
  3. Tại sao a và b là tên xấu trong context này?
[Đáp án tham khảo]
  • Bạn nghĩ:

    1. 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ế.
    2. Refactor:
      js
      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 };
      }
      Hoặc tốt hơn:
      js
      function 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 });
      }
    3. a và b không nói gì về ý nghĩa business. total và tax nói rõ đang tính gì.
  • 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

  1. 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);
  2. Hỏi AI: "Refactor đoạn code này cho dễ đọc hơn."
  3. So sánh câu trả lời AI với nhận định của bạn. AI có đổi u thành user không? AI có giải thích tại sao u.a nên thành user.isActive không? AI có sử dụng destructuring để tên rõ ràng hơn?
  4. 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.a là 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) u abbreviation. (2) u.a magic property — không biết a là gì. (3) u.n cũng vậy. Nên thành:

    js
    const activeUserNames = users
      .filter(user => user.isActive)
      .map(user => user.name);

    Hoặc dùng destructuring:

    js
    const activeUserNames = users
      .filter(({ isActive }) => isActive)
      .map(({ name }) => name);
  • AI trả lời (typical):

    • Rename u to user for clarity.
    • Use destructuring: users.filter(({ isActive }) => isActive).map(({ name }) => name).
    • Consider extracting to a named constant: const activeUserNames = ....
  • 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, x trở 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.a khô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. calculateOrderTotal thay vì calc. isUserActive thay 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õ calculateOrderTotal mộ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án calc là 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 let cho accumulator trong reduce hoặ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ải const bắ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 index i là convention vì scope cực nhỏ và ý nghĩa universal. Nhưng u trong filter(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 namingImplementation (Thực hành)Implementation Lab Level 1–3
Single responsibilityImplementation (Thực hành)Implementation Lab Level 3
Guard clause / reduce nestingImplementation (Thực hành)Implementation Lab Level 2–3
Avoid mutationPrediction (Dự đoán)Prediction Câu 3 (Transfer)
Explicitness (no magic number)Implementation (Thực hành)Implementation Lab Level 3
Refactor without behavior changeDebug LabDebug Lab Section
Compact vs explicit trade-offDesign ExerciseDesign Exercise Section
Explain readability valueTeach BackTeach 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 const và 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 + map vs for + 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 const vs let ả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ơn function fn(user).
  • Stage 8 (React) — Component naming, props destructuring, custom hooks naming convention. useActiveUsers tốt hơn useData.
  • Stage 12/14 (Architecture) — Code review, RFC, ADR. Readable code là prerequisite cho code review hiệu quả và technical decision.
📴 Offline Mode — Content served from cache