Skip to content

Lesson 0.5.7 — Function Design ​

Bài 0.5.7 — Function Design
Pure vs Impure, Single Responsibility & Composition • 28 phút
0:00 / 0:00

0. Metadata ​

FieldValue
Stage0 — JavaScript Language Foundation
Module0.5 — Functions
Lesson0.5.7
CompetencyC01.5 — Functions
Depth TargetL2–L3
PrerequisitesCallback (0.5.5), Higher-order Functions (0.5.6), Variables & Bindings (0.2.2), Object/Array basics (0.2.4)
Estimated Cognitive LoadMedium–High

1. Why This Exists (Vì sao cần học) ​

Bạn đã biết viết function ở nhiều dạng: declaration, expression, arrow, callback, HOF. Nhưng trong thực tế, viết function chạy được và viết function đúng đắn là hai việc khác nhau.

Xét đoạn code sau:

js
function processOrder(order) {
  console.log("Processing order:", order.id);
  
  if (!order.items || order.items.length === 0) {
    throw new Error("Empty order");
  }
  
  let total = 0;
  for (const item of order.items) {
    total += item.price * item.qty;
  }
  order.total = total;
  order.status = "processed";
  
  globalStats.orderCount++;
  
  return order;
}

Vấn đề cốt lõi

Function này làm 5 việc cùng lúc: log, validate, tính toán, mutate input, và thay đổi global state. Nó chạy được, nhưng:

  • Không thể test tính toán riêng biệt (phải mock console và globalStats).
  • Mutate order gây bug khi caller còn cần dữ liệu gốc.
  • Không thể tái sử dụng logic tính tổng cho chỗ khác.

Bài này dạy bạn thiết kế function như một đơn vị logic có hợp đồng rõ ràng: nhận gì, trả về gì, có làm gì bên ngoài không. Đây là bước chuyển từ "viết code chạy" sang "viết code bền vững".

2. Prerequisites (Yêu cầu đầu vào) ​

Trước khi học bài này, bạn cần:

  • Viết được function declaration, expression, arrow (0.5.1–0.5.3).
  • Hiểu return và console.log khác nhau (0.5.1).
  • Biết object và array ở mức cơ bản: tạo, truy cập property, thêm phần tử (0.2.4).
  • Biết callback và HOF ở mức sử dụng (0.5.5–0.5.6).
  • Biết const cấm reassignment nhưng không cấm mutation (0.2.2).

WARNING

Nếu bạn chưa chắc const arr = [1, 2]; arr.push(3) là hợp lệ, quay lại 0.2.2. Mutation là chủ đề trung tâm của bài này.

INFO

Bài này sử dụng cú pháp ...obj (spread) để tạo object/array mới mà không mutate bản gốc. Đây là lần đầu tiên spread xuất hiện trong khóa học — bạn sẽ đào sâu ở 0.6.8. Tạm thời, hãy hiểu { ...obj, a: 1 } nghĩa là "tạo object mới, copy hết từ obj, rồi thêm/đè a: 1".

3. Learning Objectives (Mục tiêu học tập) ​

Sau bài này, bạn có thể:

  1. Phân biệt pure function (cùng input → cùng output, không side effect) và impure function.
  2. Nhận diện side effect: mutate input, mutate global state, I/O (log, DOM, network).
  3. Tách một function lớn thành các function nhỏ có single responsibility.
  4. Viết function không mutate input argument (trả về dữ liệu mới thay vì sửa cũ).
  5. Đặt tên function theo quy tắc: động từ + đối tượng, phản ánh intent.

4. Mental Model (Mô hình tư duy) ​

Hãy hình dung hai loại máy:

Mental Model

Pure Function (Máy khép kín)
  Input ──→ [ Function ] ──→ Output
              ↓
        Không hút khí từ bên ngoài
        Không xả khói ra bên ngoài
        Cùng nguyên liệu → Cùng sản phẩm

Impure Function (Máy mở)
  Input ──→ [ Function ] ──→ Output
              ↓
        Có thể đọc/ghi bảng điều khiển chung
        Có thể sửa nguyên liệu của người khác
        Có thể cho kết quả khác cùng một nguyên liệu

Pure function là function mà:

  • Chỉ dựa vào input parameters để tính output.
  • Không làm thay đổi bất kỳ thứ gì bên ngoài.
  • Không đọc bất kỳ thứ gì bên ngoài parameters (trừ hằng số).
  • Cùng input luôn cho cùng output.

Side effect là bất kỳ thay đổi nào observable bên ngoài function:

  • Mutate input argument.
  • Mutate global variable.
  • console.log, alert.
  • DOM manipulation.
  • Network request.
  • Ghi file.

Single responsibility nghĩa là một function chỉ làm một việc có thể mô tả bằng một câu đơn giản.

5. Core Concepts (Các khái niệm cốt lõi) ​

Essential (Bắt buộc) ​

ConceptÝ nghĩa
Pure FunctionCùng input luôn cho cùng output; không có side effect.
Impure FunctionCó side effect hoặc phụ thuộc state bên ngoài parameters.
Side EffectThay đổi observable bên ngoài function (I/O, mutation, global).
Single ResponsibilityMột function chỉ làm một nhiệm vụ có thể diễn đạt rõ ràng.
Input/Output ContractFunction nhận rõ input, trả về rõ output, không "lén lút" làm việc khác.

Supporting (Hỗ trợ) ​

ConceptÝ nghĩa
Naming ConventionĐộng từ + đối tượng: calculateTotal, isValid, formatDate. Boolean: isXxx, hasXxx, canXxx.
CompositionKết hợp nhiều pure function nhỏ thành luồng xử lý lớn.

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

ConceptLý do chưa đào sâu
Referential TransparencyThay function call bằng kết quả không thay đổi behavior. Thuộc functional programming theory.
MemoizationCache kết quả pure function. Sẽ gặp lại ở Stage 11 (Performance).

Out of Scope (Không thuộc bài này) ​

  • Closure / lexical scope (Stage 1)
  • Deep clone / structured clone (Stage 2 / 11)
  • Advanced FP (currying, monad, functor — Elective)
  • Unit testing framework (Stage 9)
  • Dependency injection (Stage 12)

6. Worked Example (Ví dụ phân tích từng bước) ​

Xét đoạn code "trước" và "sau" refactoring:

js
function processUser(user) {
  console.log("Processing:", user.name);
  
  if (!user.email?.includes("@")) {
    throw new Error("Invalid email");
  }
  
  let score = 0;
  for (const order of user.orders) {
    score += order.amount;
  }
  
  user.displayName = user.name.toUpperCase();
  user.loyaltyScore = score;
  user.processedAt = new Date().toISOString();
  
  globalStats.processedCount++;
  
  return user;
}
js
function validateUser(user) {
  if (!user.email?.includes("@")) {
    throw new Error("Invalid email");
  }
}

function calculateLoyalty(orders) {
  return orders.reduce((sum, o) => sum + o.amount, 0);
}

function buildProfile(user) {
  return {
    ...user,
    displayName: user.name.toUpperCase(),
    loyaltyScore: calculateLoyalty(user.orders),
    processedAt: new Date().toISOString()
  };
}

// Usage
validateUser(user);
const profile = buildProfile(user);
console.log("Processed:", profile.name);
globalStats.processedCount++;

Walkthrough

Step 1 — Identify Responsibilities (Before)processUser làm 5 việc:

  1. Log (side effect)
  2. Validate (logic)
  3. Calculate loyalty (logic)
  4. Mutate input object (side effect)
  5. Mutate global stats (side effect)

Step 2 — Tách ValidationvalidateUser chỉ kiểm tra. Nếu fail, throw. Nếu pass, không return gì (hoặc return true). Trách nhiệm duy nhất: kiểm tra tính hợp lệ.

Step 3 — Tách CalculationcalculateLoyalty nhận orders, trả về con số. Pure function: không đụng vào user, không log, không mutate.

Step 4 — Tách Build ProfilebuildProfile tạo object mới bằng spread (...user). Không mutate user gốc. Trả về dữ liệu mới. Có thể test riêng: cho user mock, kiểm tra output object.

Step 5 — Side Effects Tập Trung Ở Callerconsole.log và globalStats++ được đẩy lên caller. Lý do: caller biết context (có nên log không? có nên đếm không?). Function core giữ pure.

Key Insight: Pure function dễ test, dễ tái sử dụng, dễ lý do. Impure code không phải "xấu" — nó chỉ nên ở biên (boundary), không lẫn trong logic core.

7. Prediction Exercise (Bài tập dự đoán) ​

Đừng chạy code. Đọc và dự đoán output, sau đó xác định function là pure hay impure và giải thích tại sao.

Câu 1 ​

js
let count = 0;
function add(a, b) {
  count++;
  return a + b;
}
console.log(add(2, 3), count);

Câu 2 ​

js
function formatPrice(amount, currency = "VND") {
  return amount.toLocaleString() + " " + currency;
}
console.log(formatPrice(1500000));

Câu 3 ​

js
const config = { theme: "dark" };
function setTheme(t) {
  config.theme = t;
  return config;
}
setTheme("light");
console.log(config.theme);

Câu 4 ​

js
function double(arr) {
  for (let i = 0; i < arr.length; i++) {
    arr[i] = arr[i] * 2;
  }
  return arr;
}
const nums = [1, 2];
console.log(double(nums));
console.log(nums);
[Đáp án & Giải thích]

Câu 1: 5 1 — Impure

  • Giải thích: Function đọc/ghi count (global). Mỗi lần gọi count tăng. Cùng input (2, 3) nhưng count thay đổi bên ngoài → impure.

Câu 2: "1,500,000 VND" — Pure

  • Giải thích: Chỉ dùng parameters. Không đọc/ghi bên ngoài. Cùng input luôn cho cùng output. toLocaleString() có thể khác nhau theo locale, nhưng trong cùng một runtime environment, với cùng input, output nhất quán.

Câu 3: "light" — Impure

  • Giải thích: Mutate config (global object). setTheme không chỉ trả về object mà còn thay đổi state bên ngoài.

Câu 4: [2, 4] và [2, 4] — Impure

  • Giải thích: double mutate input array trực tiếp. nums bị thay đổi sau khi gọi. Đây là side effect nguy hiểm nhất trong JavaScript: caller mất dữ liệu gốc.

8. Implementation Lab (Bài lab thực hành) ​

Level 1 — Guided (Có hướng dẫn) ​

Refactor function sau thành pure function. Không được mutate cart.

js
function addItem(cart, item) {
  cart.items.push(item);
  cart.total += item.price;
  return cart;
}

Gợi ý

Dùng spread để tạo object mới: { ...cart, items: [...cart.items, item] }. Tính total bằng cách reduce hoặc cộng thủ công.

[Đáp án tham khảo]
js
function addItem(cart, item) {
  const newItems = [...cart.items, item];

  const total = newItems.reduce((sum, item) => {
    return sum + item.price;
  }, 0);

  return {
    ...cart,
    items: newItems,
    total
  };
}

const cart = {
  items: [
    { name: "Book", price: 100 }
  ],
  total: 100
};

const updatedCart = addItem(cart, {
  name: "Pen",
  price: 20
});

console.log(cart);
// {
//   items: [{ name: "Book", price: 100 }],
//   total: 100
// }

console.log(updatedCart);
// {
//   items: [
//     { name: "Book", price: 100 },
//     { name: "Pen", price: 20 }
//   ],
//   total: 120
// }

Giải thích:

  • Không dùng push() vì push() sẽ mutate mảng cart.items.
  • [...cart.items, item] tạo ra một mảng mới.
  • { ...cart, ... } tạo ra một object cart mới.
  • reduce() được dùng để tính lại total.
  • Function không thay đổi cart ban đầu nên đây là một pure function.

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

Tách function "khổng lồ" sau thành 3 function nhỏ: validate, calculate, build.

js
function processInvoice(invoice) {
  if (!invoice || !invoice.items) {
    throw new Error("Invalid invoice");
  }
  
  let total = 0;
  for (const item of invoice.items) {
    total += item.price * item.quantity;
  }
  
  return {
    ...invoice,
    total,
    status: total > 1000 ? "pending_review" : "approved"
  };
}

// Tách thành:
function validateInvoice(invoice) { /* ... */ }
function calculateTotal(items) { /* ... */ }
function buildInvoice(invoice, total) { /* ... */ }
[Đáp án tham khảo]
js
function validateInvoice(invoice) {
  if (!invoice || !invoice.items) {
    throw new Error("Invalid invoice");
  }

  return invoice;
}

function calculateTotal(items) {
  return items.reduce((total, item) => {
    return total + item.price * item.quantity;
  }, 0);
}

function buildInvoice(invoice, total) {
  return {
    ...invoice,
    total,
    status: total > 1000 ? "pending_review" : "approved"
  };
}

function processInvoice(invoice) {
  validateInvoice(invoice);

  const total = calculateTotal(invoice.items);

  return buildInvoice(invoice, total);
}

Giải thích:

  • validateInvoice() chỉ chịu trách nhiệm kiểm tra dữ liệu đầu vào.
  • calculateTotal() chỉ chịu trách nhiệm tính tổng tiền.
  • buildInvoice() tạo object invoice mới dựa trên invoice và total.
  • processInvoice() đóng vai trò điều phối 3 bước theo thứ tự: validate → calculate → build.
  • Việc chia function giúp mỗi function có một trách nhiệm rõ ràng, dễ đọc và dễ kiểm thử hơn.

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

Viết một module xử lý danh sách task gồm các pure function:

js
// 1. addTask(tasks, newTask) → trả về mảng mới (không mutate tasks)
// 2. completeTask(tasks, taskId) → trả về mảng mới với task có id tương ứng đổi status
// 3. getPendingTasks(tasks) → trả về mảng các task chưa hoàn thành
// 4. countByStatus(tasks) → trả về object { pending: n, completed: m }

// Sử dụng:
const tasks = [
  { id: 1, title: "A", status: "pending" },
  { id: 2, title: "B", status: "completed" }
];

const updated = addTask(tasks, { id: 3, title: "C", status: "pending" });
console.log(tasks.length); // vẫn là 2 (không bị mutate)
console.log(updated.length); // 3
[Đáp án tham khảo]
js
function addTask(tasks, newTask) {
  return [...tasks, newTask];
}

function completeTask(tasks, taskId) {
  return tasks.map(task => {
    if (task.id === taskId) {
      return {
        ...task,
        status: "completed"
      };
    }

    return task;
  });
}

function getPendingTasks(tasks) {
  return tasks.filter(task => task.status === "pending");
}

function countByStatus(tasks) {
  return tasks.reduce(
    (result, task) => {
      result[task.status]++;
      return result;
    },
    {
      pending: 0,
      completed: 0
    }
  );
}

const tasks = [
  { id: 1, title: "A", status: "pending" },
  { id: 2, title: "B", status: "completed" }
];

const updated = addTask(tasks, {
  id: 3,
  title: "C",
  status: "pending"
});

console.log(tasks.length);
// 2

console.log(updated.length);
// 3

const completed = completeTask(tasks, 1);

console.log(completed);
// [
//   { id: 1, title: "A", status: "completed" },
//   { id: 2, title: "B", status: "completed" }
// ]

console.log(getPendingTasks(tasks));
// [
//   { id: 1, title: "A", status: "pending" }
// ]

console.log(countByStatus(tasks));
// {
//   pending: 1,
//   completed: 1
// }

Giải thích:

  • addTask() dùng spread để tạo mảng mới, không thay đổi tasks.
  • completeTask() dùng map() và spread để tạo task mới khi tìm thấy taskId.
  • getPendingTasks() dùng filter() để lấy các task có status là "pending".
  • countByStatus() dùng reduce() để đếm số lượng task theo từng status.
  • Tất cả function đều không mutate dữ liệu đầu vào nên có tính chất của pure function.

9. Edge Cases (Các trường hợp ngoại lệ) ​

Shallow copy không đủ với nested object

js
function updateUser(user) {
  return { ...user, profile: user.profile };
}
updateUser(u).profile.age = 30; // Mutate original!

Tại sao: profile vẫn là cùng một reference. Spread chỉ copy top-level.

Cách nhận biết: Object con bị thay đổi dù parent là "mới".

Cách xử lý: Ở Stage 0, nhận biết giới hạn của spread. Deep clone sẽ học ở Stage 2/11. Tạm thời: nếu cần đảm bảo pure với nested, tạo new object cho từng level.

"Pure" nhưng dùng Date.now()

js
function createId() {
  return "id_" + Date.now();
}

Tại sao: Date.now() đọc global clock. Cùng input (không có) nhưng output khác nhau mỗi lần gọi.

Cách nhận biết: Không có parameter nhưng kết quả thay đổi.

Cách xử lý: Đưa timestamp vào parameter để caller kiểm soát.

Mutate trong forEach

js
function normalize(items) {
  items.forEach(item => item.name = item.name.trim());
  return items;
}

Tại sao: forEach không return nhưng callback bên trong mutate từng phần tử.

Cách nhận biết: Input array bị thay đổi sau khi gọi.

Cách xử lý: Dùng map để tạo array mới:

js
function normalize(items) {
  return items.map(item => ({ ...item, name: item.name.trim() }));
}

Tên function không phản ánh side effect

js
function getUser(user) {
  user.lastAccess = new Date();
  return user;
}

Tại sao: Tên getUser gợi ý "lấy dữ liệu" (read-only). Nhưng function lại mutate input.

Cách nhận biết: Review code không phát hiện mutation vì tên quá "vô tội".

Cách xử lý: Đặt tên rõ ràng: updateLastAccess, recordAccess, touchUser.

10. Debug Lab (Bài lab gỡ lỗi) ​

Symptom (Triệu chứng): Danh sách tag bị "lây nhiễm" giữa các sản phẩm.

js
function addTag(product, tag) {
  product.tags.push(tag);
  return product;
}

const baseProduct = { id: 1, name: "Shirt", tags: ["new"] };
const p1 = addTag(baseProduct, "sale");
const p2 = addTag(baseProduct, "bestseller");

console.log(p1.tags); // ["new", "sale", "bestseller"] ❌
console.log(p2.tags); // ["new", "sale", "bestseller"] ❌

Reproduction (Tái hiện lỗi): Chạy code trên.

Evidence (Bằng chứng):

  • p1 và p2 đều có cùng 3 tags dù mỗi cái chỉ thêm 1 tag.
  • baseProduct.tags cũng bị thay đổi.

Hypothesis (Giả thuyết): addTag mutate product.tags trực tiếp qua .push(). Vì p1 và p2 đều trỏ đến cùng baseProduct, mọi mutation tích lũy lên cùng một object.

Verification (Xác minh): Kiểm tra reference:

js
console.log(p1 === baseProduct); // true
console.log(p1.tags === p2.tags); // true

Root Cause (Nguyên nhân gốc rễ): Function tưởng chừng "thêm tag và trả về product" nhưng thực chất mutate input. Đây là side effect ẩn — tên function (addTag) không cảnh báo về mutation.

Fix (Sửa):

js
function addTag(product, tag) {
  return {
    ...product,
    tags: [...product.tags, tag]
  };
}

Prevention (Phòng ngừa):

  • Mặc định nghi ngờ mọi function mutate input. Nếu cần mutate, đặt tên rõ ràng: pushTagToProduct hoặc dùng comment.
  • Dùng spread để tạo bản sao khi return.
  • Trong code review, kiểm tra các method như .push(), .pop(), .splice() — chúng là dấu hiệu mutation.

11. Design Exercise (Bài tập thiết kế giải pháp) ​

Bạn cần viết một hàm xử lý đăng ký người dùng:

js
function validateRegistration(data) {
  if (!data.email?.includes("@")) return { ok: false, error: "Invalid email" };
  if (!data.password || data.password.length < 8) return { ok: false, error: "Password too short" };
  return { ok: true, data };
}

function createUserRecord(data) {
  return {
    id: generateId(),
    email: data.email.toLowerCase(),
    createdAt: new Date().toISOString()
  };
}

// Caller (controller):
const validation = validateRegistration(req.body);
if (!validation.ok) return { status: 400, message: validation.error };

const user = createUserRecord(req.body);
await db.users.insert(user);
logger.info("User registered", user.id);
js
function registerUser(data) {
  if (!data.email?.includes("@")) throw new Error("Invalid email");
  if (!data.password || data.password.length < 8) throw new Error("Password too short");
  
  const user = {
    id: generateId(),
    email: data.email.toLowerCase(),
    createdAt: new Date().toISOString()
  };
  
  db.users.insert(user);
  logger.info("User registered", user.id);
  
  return user;
}

Câu hỏi:

  1. Option A có lợi gì cho việc unit test?
  2. Option B có lợi gì cho việc sử dụng?
  3. Nếu db.users.insert bị lỗi network, Option B mất dữ liệu gì? Option A thì sao?
[Đáp án tham khảo]
  • Bạn nghĩ:

    1. Option A: validateRegistration và createUserRecord đều pure (hoặc gần pure). Có thể test mà không cần mock database hay logger.
    2. Option B: Gọn hơn, caller chỉ cần một dòng. Ít decision fatigue hơn.
    3. Option B: Nếu db.insert fail, function throw nhưng user object đã được tạo — caller không nhận được (vì throw). Tuy nhiên, logger có thể đã ghi log trước khi insert fail. Option A: caller kiểm soát từng bước. Nếu insert fail, vẫn có user object trong scope để xử lý (retry, log lỗi chi tiết).
  • Kết luận: Pure function ở core + side effect ở boundary là pattern phổ biến trong production. Nó tách "quyết định" khỏi "tác động", giúp test và debug dễ hơn.

12. Production Scenario (Tình huống thực tế) ​

Context: Một utility function getDisplayName được dùng ở 20 chỗ trong app:

js
const cache = {};

function getDisplayName(user) {
  if (cache[user.id]) {
    return cache[user.id];
  }
  const name = user.nickname || user.name || "Guest";
  cache[user.id] = name;
  return name;
}

Symptom: User thay đổi nickname trong profile. App vẫn hiển thị nickname cũ ở một số màn hình. Hard refresh không fix. Chỉ có restart server (Node.js) hoặc đổi tab mới (Browser) mới fix.

Constraint: Không được xóa cache vì team nói nó giúp "tăng performance".

Câu hỏi:

  1. Tại sao nickname cũ vẫn hiển thị?
  2. getDisplayName là pure hay impure? Tại sao?
  3. Đề xuất một cách giữ lợi ích cache mà không gây bug stale data.
[Đáp án tham khảo]
  • Bạn nghĩ:

    1. cache là global object. Lần đầu gọi getDisplayName với user.id = 1, nickname được lưu vào cache. Khi user đổi nickname, cache[user.id] vẫn giữ giá trị cũ. Function đọc cache thay vì tính toán lại.
    2. Impure. Đọc/ghi cache (global state). Cùng input (user object với cùng id) nhưng output khác nhau tùy thời điểm (trước và sau khi cache warm).
    3. Đưa cache vào caller hoặc dùng memoization có TTL:

    cache không còn là hidden global state. Caller nhìn vào function signature là biết: “Hàm này phụ thuộc vào một cache được truyền vào.”. Caller kiểm soát lifecycle của cache ( tạo mới, reset ) và cũng dễ test hơn

    js
    function getDisplayName(user, cache = {}) {
      if (cache[user.id]) return cache[user.id];
      const name = user.nickname || user.name || "Guest";
      cache[user.id] = name;
      return name;
    }

    Hoặc tách thành pure function + cache layer riêng biệt.

  • Bài học: Cache + impure function = stale data bug. Cache nên là một layer riêng biệt, không nẫu trong logic core.

    Phần mở rộng

    Tách thành pure function + cache layer riêng biệt.

    js
    function getDisplayName(user) {
      return user.nickname || user.name || "Guest";
    }
    
    function getCachedDisplayName(user, cache) {
      if (cache[user.id]) return cache[user.id];
    
      const name = getDisplayName(user);
      cache[user.id] = name;
    
      return name;
    }

    Vài cách phổ biến để cache invalidation

      1. Xóa cache ngay khi update — đơn giản nhất
      js
      const cache = {};
      
      function getDisplayName(user) {
        if (cache[user.id]) return cache[user.id];
      
        const name = user.nickname || user.name || "Guest";
        cache[user.id] = name;
        return name;
      }
      
      function updateNickname(user, nickname) {
        user.nickname = nickname;
      
        // invalidate
        delete cache[user.id];
      }
      1. Update cache luôn thay vì xóa Nếu đã biết giá trị mới:
        js
        function updateNickname(user, nickname) {
          user.nickname = nickname;
          cache[user.id] = nickname;
        }
        Không cần chờ lần getDisplayName() tiếp theo tính lại. Nhưng cách này dễ phức tạp nếu display name không đơn giản chỉ là nickname.
      1. TTL — tự hết hạn sau một khoảng thời gian Ví dụ cache 5 phút:
        js
        const cache = {};
        
        function getDisplayName(user) {
          const cached = cache[user.id];
        
          if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) {
            return cached.value;
          }
        
          const value = user.nickname || user.name || "Guest";
        
          cache[user.id] = {
            value,
            timestamp: Date.now()
          };
        
          return value;
        }
        • Ưu điểm: nếu quên invalidate, dữ liệu cũ không tồn tại mãi.

        • Nhược điểm: trong 5 phút đó vẫn có thể stale.

      1. Version / revision: Đây là cách hay khi dữ liệu có updatedAt hoặc version.
        js
        const cache = {};
        
        function getDisplayName(user) {
          const cached = cache[user.id];
        
          if (cached && cached.version === user.version) {
            return cached.value;
          }
        
          const value = user.nickname || user.name || "Guest";
        
          cache[user.id] = {
            value,
            version: user.version
          };
        
          return value;
        }

13. AI-assisted Exercise (Bài tập với AI) ​

Level B — Challenge

  1. Tự trả lời trước: Viết một ví dụ về function JavaScript mà AI có thể nghĩ là pure nhưng thực ra là impure (gợi ý: mutate input object).
  2. Hỏi AI: "Is this function pure?"
js
function addItem(cart, item) { 
  cart.items.push(item); 
  return cart; 
}
  1. So sánh câu trả lời AI với nhận định của bạn. AI có nhận ra .push() là mutation không?
  2. Verify bằng MDN: tìm Array.prototype.push trên MDN và đọc phần "Return value" + "Description".

Gợi ý

AI thường trả lời đúng về pure/impure ở mức surface. Nhưng khi gặp .push(), AI đôi khi nói "it returns the cart with the item added" mà không nhấn mạnh rằng .push() mutate array gốc. Nếu AI không chỉ ra caller mất dữ liệu gốc, bạn đã tìm ra điểm mù quan trọng.

Đáp án tham khảo
  • Bạn nghĩ: Function impure vì cart.items.push(item) mutate input array. Caller mất array gốc.

  • AI trả lời (typical):

    • This function is not pure because it mutates the cart.items array.
    • It modifies the input object directly.
    • A pure version would create a new array.
  • So sánh: AI thường nhận ra mutation nhưng đôi khi chỉ nói "not pure" mà không giải thích hậu quả production: nếu cart được dùng ở nơi khác (ví dụ: so sánh giỏ hàng trước/sau), mutation làm mất khả năng so sánh.

  • Điểm AI nói sai hoặc quá mơ hồ: "It modifies the input object directly" — đúng nhưng chưa đủ. AI hiếm khi nói rõ: "Nếu caller còn giữ reference đến cart gốc để làm việc khác (ví dụ: tính tổng tiền cũ), mutation sẽ làm kết quả sai."

  • Kết luận: Nếu bạn chỉ ra được rằng mutation không chỉ vi phạm "pure" mà còn gây bug silent khi caller reuse data, bạn đã hiểu sâu hơn AI. Concept này sẽ quay lại ở Stage 2 (Object Model), Stage 8 (React state immutability), và Stage 11 (Memoization).

14. Teach Back (Dạy lại) ​

Giả sử một junior developer hỏi bạn:

"Em thấy người ta bảo viết pure function tốt hơn. Nhưng function của em cần ghi log để debug, vậy em có phải bỏ console.log không? Và nếu em không được mutate object, em phải copy cả object lớn — không chậm sao?"

Hãy giải thích trong 2 phút, dùng khái niệm: boundary vs core, single responsibility, predictability.

Mô phỏng
  • Bạn nói: Bạn không phải bỏ console.log. Quy tắc là: side effect tập trung ở biên (boundary), logic core giữ pure.

Ví dụ: function tính tổng tiền nên pure — chỉ nhận giá trị, trả về kết quả. Còn console.log để ở chỗ gọi nó. Như vậy bạn vẫn log được mà logic tính toán vẫn dễ test. Về copy object: đúng là có chi phí, nhưng ở Stage 0 chúng ta học shallow copy (...obj). Nếu object lớn đến mức copy ảnh hưởng performance, đó là vấn đề của Stage 11. Còn 90% thời gian, bug do mutation tốn đắt hơn performance do copy rất nhiều. Hãy đúng trước, rồi mới nhanh.

💡 Tưởng tượng pure function như một máy tính Casio: bạn bấm 5 + 3, nó hiện 8. Nó không gửi tin nhắn cho ai, không ghi vào sổ điều khiển của nhà máy. Nếu bạn cần ghi sổ, hãy ghi ở ngoài máy tính, không nhét sổ vào trong máy.

Gợi ý đánh giá bản thân

  • Đồng nghiệp có hiểu "biên" và "core" khác nhau không?
  • Bạn có tránh được việc nói "pure function là không có side effect" một cách tuyệt đối không? (Pure function không có side effect, nhưng ứng dụng thì có — nó ở layer khác.)
  • Nếu đồng nghiệp hỏi: "Vậy console.log trong function tính toán có tội gì?" — bạn trả lời được không? (Gợi ý: làm function không thể test trong môi trường không có console, và làm output không xác định - deterministic.)

15. Assessment (Đánh giá) ​

Objective (Mục tiêu)Hình thức đánh giáTask
Identify pure vs impure (Nhận diện pure/impure)Prediction (Dự đoán)Prediction Exercise
Refactor to pure function (Refactor thành pure)Implementation (Thực hành)Implementation Lab Level 1
Separate responsibilities (Tách trách nhiệm)Implementation (Thực hành)Implementation Lab Level 2
Debug input mutation (Gỡ lỗi mutate input)Debug LabDebug Lab Section
Choose pure vs impure architecture (Chọn kiến trúc)Design DecisionDesign Exercise
Name function by intent (Đặt tên function đúng intent)Implementation (Thực hành)Implementation Lab Level 2 + Level 3 (rubric kiểm tra tên function)

16. Exit Criteria (Tiêu chí qua bài) ​

  • [ ] Có thể phân biệt pure function và impure function dựa trên 3 tiêu chí: cùng input → cùng output, không side effect, không đọc state bên ngoài.
  • [ ] Có thể nhận diện 4 loại side effect phổ biến: mutate input, mutate global, I/O, đọc global state.
  • [ ] Có thể refactor một function mutate input thành function trả về dữ liệu mới bằng spread.
  • [ ] Có thể tách một function lớn thành các function nhỏ có single responsibility.
  • [ ] Có thể đặt tên function phản ánh đúng intent (động từ + đối tượng, boolean prefix).
  • [ ] Có thể giải thích tại sao side effect nên tập trung ở boundary thay vì lẫn trong logic core.

17. Spiral Connection (Liên kết xoắn ốc) ​

Previous (Trước): Higher-order Functions (0.5.6) — bạn đã biết tạo và kết hợp function. Giờ bạn học cách thiết kế từng function sao cho đúng đắn về mặt engineering.

Current (Hiện tại): Function Design — pure vs impure, side effect, single responsibility, naming, composition. Đây là bài kết thúc Module 0.5.

Next (Tiếp theo):

  • 0.6.1–0.6.8 — Data Structures: bạn sẽ áp dụng pure function pattern lên String, Array, Object (không mutate, trả về mới).
  • Stage 1 — Execution Model: hiểu tại sao function "nhớ" được biến từ scope ngoài (closure) — cơ chế đằng sau HOF factory.
  • Stage 8 — React: state immutability là quy tắc sống còn. Không bao giờ mutate state trực tiếp.
  • Stage 9 — Testing: pure function dễ unit test nhất vì không cần mock.
  • Stage 11 — Performance: memoization chỉ hoạt động hiệu quả với pure function.
  • Stage 12 — Architecture: separation of concerns và boundary/core pattern ở cấp hệ thống.
📴 Offline Mode — Content served from cache