Lesson 0.5.7 — Function Design
0. Metadata
| Field | Value |
|---|---|
| Stage | 0 — JavaScript Language Foundation |
| Module | 0.5 — Functions |
| Lesson | 0.5.7 |
| Competency | C01.5 — Functions |
| Depth Target | L2–L3 |
| Prerequisites | Callback (0.5.5), Higher-order Functions (0.5.6), Variables & Bindings (0.2.2), Object/Array basics (0.2.4) |
| Estimated Cognitive Load | Medium–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:
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
consolevàglobalStats). - Mutate
ordergâ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
returnvàconsole.logkhá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
constcấ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ể:
- Phân biệt pure function (cùng input → cùng output, không side effect) và impure function.
- Nhận diện side effect: mutate input, mutate global state, I/O (log, DOM, network).
- Tách một function lớn thành các function nhỏ có single responsibility.
- Viết function không mutate input argument (trả về dữ liệu mới thay vì sửa cũ).
- Đặ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ệuPure 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 Function | Cùng input luôn cho cùng output; không có side effect. |
| Impure Function | Có side effect hoặc phụ thuộc state bên ngoài parameters. |
| Side Effect | Thay đổi observable bên ngoài function (I/O, mutation, global). |
| Single Responsibility | Một function chỉ làm một nhiệm vụ có thể diễn đạt rõ ràng. |
| Input/Output Contract | Function 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. |
| Composition | Kết hợp nhiều pure function nhỏ thành luồng xử lý lớn. |
Awareness (Biết tồn tại)
| Concept | Lý do chưa đào sâu |
|---|---|
| Referential Transparency | Thay function call bằng kết quả không thay đổi behavior. Thuộc functional programming theory. |
| Memoization | Cache 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:
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;
}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:
- Log (side effect)
- Validate (logic)
- Calculate loyalty (logic)
- Mutate input object (side effect)
- 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
let count = 0;
function add(a, b) {
count++;
return a + b;
}
console.log(add(2, 3), count);Câu 2
function formatPrice(amount, currency = "VND") {
return amount.toLocaleString() + " " + currency;
}
console.log(formatPrice(1500000));Câu 3
const config = { theme: "dark" };
function setTheme(t) {
config.theme = t;
return config;
}
setTheme("light");
console.log(config.theme);Câu 4
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ọicounttăng. Cùng input(2, 3)nhưngcountthay đổ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).setThemekhô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:
doublemutate input array trực tiếp.numsbị 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.
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]
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ảngcart.items. [...cart.items, item]tạo ra một mảng mới.{ ...cart, ... }tạo ra một objectcartmới.reduce()được dùng để tính lạitotal.- Function không thay đổi
cartban đầ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.
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]
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êninvoicevà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:
// 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]
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 đổitasks.completeTask()dùngmap()và spread để tạo task mới khi tìm thấytaskId.getPendingTasks()dùngfilter()để lấy các task cóstatuslà"pending".countByStatus()dùngreduce()để đế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
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()
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
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:
function normalize(items) {
return items.map(item => ({ ...item, name: item.name.trim() }));
}Tên function không phản ánh side effect
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.
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):
p1vàp2đều có cùng 3 tags dù mỗi cái chỉ thêm 1 tag.baseProduct.tagscũ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:
console.log(p1 === baseProduct); // true
console.log(p1.tags === p2.tags); // trueRoot 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):
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:
pushTagToProducthoặ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:
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);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:
- Option A có lợi gì cho việc unit test?
- Option B có lợi gì cho việc sử dụng?
- Nếu
db.users.insertbị lỗi network, Option B mất dữ liệu gì? Option A thì sao?
[Đáp án tham khảo]
Bạn nghĩ:
- Option A:
validateRegistrationvàcreateUserRecordđều pure (hoặc gần pure). Có thể test mà không cần mock database hay logger. - Option B: Gọn hơn, caller chỉ cần một dòng. Ít decision fatigue hơn.
- Option B: Nếu
db.insertfail, function throw nhưnguserobject đã đượ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óuserobject trong scope để xử lý (retry, log lỗi chi tiết).
- Option A:
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:
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:
- Tại sao nickname cũ vẫn hiển thị?
getDisplayNamelà pure hay impure? Tại sao?- Đề 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ĩ:
cachelà global object. Lần đầu gọigetDisplayNamevớiuser.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.- Impure. Đọc/ghi
cache(global state). Cùng input (userobject với cùngid) nhưng output khác nhau tùy thời điểm (trước và sau khi cache warm). - Đưa cache vào caller hoặc dùng memoization có TTL:
cachekhô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ơnjsfunction 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.
jsfunction 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
- Xóa cache ngay khi update — đơn giản nhất
jsconst 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]; }- Update cache luôn thay vì xóa Nếu đã biết giá trị mới:jsKhông cần chờ lần
function updateNickname(user, nickname) { user.nickname = nickname; cache[user.id] = nickname; }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.
- Update cache luôn thay vì xóa Nếu đã biết giá trị mới:
- 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.
- TTL — tự hết hạn sau một khoảng thời gian Ví dụ cache 5 phút:
- Version / revision: Đây là cách hay khi dữ liệu có
updatedAthoặcversion.jsconst 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; }
- Version / revision: Đây là cách hay khi dữ liệu có
13. AI-assisted Exercise (Bài tập với AI)
Level B — Challenge
- 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).
- Hỏi AI: "Is this function pure?"
function addItem(cart, item) {
cart.items.push(item);
return cart;
}- 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? - Verify bằng MDN: tìm
Array.prototype.pushtrê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.itemsarray. - It modifies the input object directly.
- A pure version would create a new array.
- This function is not pure because it mutates the
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
cartgố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.logkhô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ện8. 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.logtrong 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 Lab | Debug Lab Section |
| Choose pure vs impure architecture (Chọn kiến trúc) | Design Decision | Design 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.