Lesson 1.4.4 — Factory Functions
0. Metadata (Thông tin bài học)
| Field | Value |
|---|---|
| Stage | 1 |
| Module | 1.4 — Closures |
| Lesson | 1.4.4 — Factory Functions |
| Competency | C02 — JavaScript Runtime (C02.5 Closure) |
| Depth Target | L5 (Implement) |
| Prerequisites | Closure Formation (1.4.1), Closure Lifetime (1.4.2), Private State (1.4.3), Lexical Environment (1.2.7), Variable Resolution (1.2.6) |
| Estimated Cognitive Load | High |
Out of Scope (Ngoài phạm vi)
- Constructor function,
new, prototype chain (Stage 2) classvà object-oriented design (Stage 2)- Async callbacks và scheduling (Stage 3)
- React state / stale closure (Stage 8)
- Garbage Collection internals và memory profiling (Stage 11)
1. Why This Exists (Vì sao cần học)
Ở Lesson 1.4.3, bạn đã dùng closure để tạo một createCounter() có private state. Nhưng production code hiếm khi chỉ cần đúng một counter. Ta thường cần một cách tạo ra nhiều instance có cùng behavior nhưng state độc lập:
const userA = createUser("Alice");
const userB = createUser("Bob");Cả userA và userB được tạo từ cùng một function. Vậy tại sao thay đổi state của userA không làm state của userB thay đổi?
Đó là vấn đề Factory Functions giải quyết trong module này: dùng mỗi invocation của factory để tạo một environment riêng, rồi trả về các function cùng truy cập state của environment đó.
Nếu không hiểu cơ chế này, bạn sẽ dễ:
- Đặt state sai scope và vô tình chia sẻ giữa nhiều instance.
- Nghĩ rằng object mới đồng nghĩa với state mới.
- Không giải thích được tại sao hai method trong cùng instance chia sẻ private state.
- Debug sai khi nhiều instance ảnh hưởng lẫn nhau.
- Dùng factory như một pattern thuộc lòng thay vì hiểu nó từ closure.
Terminology
Factory function là một function tạo và trả về một value/object theo một contract. Factory không bắt buộc phải dùng closure.
Trong lesson này, ta tập trung vào closure-based factory vì mục tiêu của Module 1.4 là hiểu Closure.
2. Prerequisites (Yêu cầu đầu vào)
Trước khi học bài này, bạn phải:
- [ ] Giải thích được inner function giữ reference đến lexical environment nơi nó được tạo.
- [ ] Phân biệt execution context bị pop với lexical environment còn được closure giữ lại.
- [ ] Biết mỗi lần gọi một function tạo ra một invocation riêng.
- [ ] Implement được private state bằng closure như
createCounter(). - [ ] Phân biệt local binding với binding ở outer/global scope.
Nếu thiếu, quay lại Lesson 1.4.1–1.4.3 trước.
3. Learning Objectives (Mục tiêu học tập)
Sau bài này, bạn có thể:
- Giải thích tại sao mỗi lần gọi factory có thể tạo một state độc lập.
- Vẽ environment diagram cho hai instance được tạo từ cùng một factory.
- Dự đoán behavior khi state nằm trong factory và khi state vô tình nằm ngoài factory.
- Implement
createUser(),createLogger()vàcreateStore()bằng closure. - Debug bug shared state giữa nhiều factory instance.
- Thiết kế một factory API nhỏ giữ state private và bảo vệ invariant.
4. Mental Model (Mô hình tư duy)
Mental Model — Factory Functions
Mỗi lần factory được gọi, một invocation mới được tạo. Invocation đó có lexical environment và local bindings riêng. Các function được tạo bên trong invocation có thể giữ reference đến environment đó.
Vì vậy:
cùng một factory → nhiều invocation → nhiều environment → state có thể độc lập.
Ví dụ:
function createCounter() {
let count = 0;
return {
increment() {
count = count + 1;
},
getValue() {
return count;
},
};
}
const a = createCounter();
const b = createCounter();Mental model:
createCounter() — Invocation A
↓
Environment A
└── count = 0
↑
├── a.increment
└── a.getValue
createCounter() — Invocation B
↓
Environment B
└── count = 0
↑
├── b.increment
└── b.getValuea.increment và a.getValue cùng tham chiếu đến Environment A. b.increment và b.getValue cùng tham chiếu đến Environment B.
Điểm quan trọng: object mới không phải nguyên nhân cốt lõi tạo state độc lập. State độc lập đến từ việc mỗi factory invocation có local bindings riêng và closures của instance giữ đúng environment đó.
5. Core Concepts (Các khái niệm cốt lõi)
Essential (Bắt buộc)
- Factory Function: Function tạo và trả về một value/object theo một contract.
- Per-invocation Environment: Mỗi lần gọi factory tạo một invocation với local bindings riêng.
- Per-instance State: State thuộc environment của một factory invocation cụ thể.
- Shared Private State: Nhiều method được tạo trong cùng invocation có thể cùng truy cập một private binding.
- Instance Isolation: Hai instance từ hai invocation khác nhau không chia sẻ local state nếu state được khai báo bên trong factory.
Supporting (Hỗ trợ)
- Object literal có thể làm container cho public API.
- Parameters của factory có thể trở thành captured bindings.
- Closure cho phép API còn truy cập state sau khi factory đã return.
- State ownership phụ thuộc vị trí declaration của binding.
Awareness (Biết tồn tại)
- Factory có thể được implement theo nhiều cách, không chỉ closure.
- Prototype/class có cách tổ chức behavior khác và sẽ học ở Stage 2.
- Closure-based factory có trade-off về allocation/memory nhưng chưa phân tích ở Stage 1.
6. Worked Example (Ví dụ phân tích từng bước)
Worked Example — Trace hai instance từ cùng một factory:
function createUser(name) {
let loginCount = 0;
return {
login() {
loginCount = loginCount + 1;
return `${name}: ${loginCount}`;
},
getLoginCount() {
return loginCount;
},
};
}
const alice = createUser("Alice");
const bob = createUser("Bob");
alice.login();
alice.login();
bob.login();
console.log(alice.getLoginCount());
console.log(bob.getLoginCount());Step 1 — createUser("Alice") được gọi:
- Tạo một invocation mới của
createUser. - Environment A chứa
name → "Alice"vàloginCount → 0. loginvàgetLoginCountđược tạo trong Environment A.- Hai method đều giữ reference cần thiết đến Environment A.
- Object chứa hai method được trả về và gán cho
alice.
Step 2 — createUser("Bob") được gọi:
- Đây là invocation khác, không phải dùng lại invocation trước.
- Environment B chứa
name → "Bob"vàloginCount → 0. - Hai method mới được tạo trong Environment B.
- Object mới được trả về và gán cho
bob.
Step 3 — alice.login() chạy hai lần:
alice.loginresolveloginCounttrong Environment A.- Lần 1:
loginCount_Atừ0 → 1. - Lần 2:
loginCount_Atừ1 → 2. - Environment B không bị truy cập.
Step 4 — bob.login() chạy một lần:
bob.loginresolveloginCounttrong Environment B.loginCount_Btừ0 → 1.- Environment A không thay đổi.
Step 5 — Kết quả:
2
1Hai instance độc lập vì chúng được tạo từ hai invocation khác nhau, mỗi invocation có environment và bindings riêng.
7. Prediction Exercise (Bài tập dự đoán)
Đừng chạy code. Dự đoán output và giải thích bằng invocation + environment + binding.
Prediction 1
function createCounter(start) {
let count = start;
return {
increment() {
count = count + 1;
},
getValue() {
return count;
},
};
}
const a = createCounter(0);
const b = createCounter(10);
a.increment();
a.increment();
b.increment();
console.log(a.getValue());
console.log(b.getValue());Prediction 2
let count = 0;
function createCounter() {
return {
increment() {
count = count + 1;
},
getValue() {
return count;
},
};
}
const a = createCounter();
const b = createCounter();
a.increment();
a.increment();
b.increment();
console.log(a.getValue());
console.log(b.getValue());Prediction 3
function createBox(value) {
return {
set(nextValue) {
value = nextValue;
},
get() {
return value;
},
};
}
const a = createBox("A");
const b = createBox("B");
a.set("A2");
console.log(a.get());
console.log(b.get());[Đáp án & Giải thích]
Prediction 1:
- Output:
2rồi11. - Giải thích:
avàbđến từ hai invocation khác nhau. Mỗi invocation tạo một bindingcountriêng. Hai lầna.increment()chỉ thay đổicountcủa Environment A;b.increment()chỉ thay đổicountcủa Environment B.
- Output:
Prediction 2:
- Output:
3rồi3. - Giải thích:
countđược khai báo bên ngoài factory. Hai invocation tạo ra các method khác nhau, nhưng các method đều resolve cùng một outer bindingcount. Object khác nhau không làm binding bên ngoài tự tách thành hai state.
- Output:
Prediction 3:
- Output:
"A2"rồi"B". - Giải thích: Mỗi invocation của
createBoxcó parameter bindingvalueriêng.setvàgetcủaacùng dùng Environment A;setvàgetcủabcùng dùng Environment B.
- Output:
Transfer Check — không cần biết pattern này trước:
function createToggle(initialState) {
let active = initialState;
return {
toggle() {
active = !active;
},
isActive() {
return active;
},
};
}
const menu = createToggle(false);
const modal = createToggle(false);
menu.toggle();
console.log(menu.isActive());
console.log(modal.isActive());Hãy trả lời:
- Có bao nhiêu factory invocation?
- Có bao nhiêu binding
active? menu.toggle()thay đổi binding nào?- Output cuối cùng là gì?
Gợi ý trả lời
- Có hai invocation của
createToggle. - Có hai binding
active, mỗi binding thuộc một invocation. menu.toggle()thay đổiactivetrong environment được closures củamenugiữ lại.- Output là
truerồifalse.
8. Implementation Lab (Bài lab thực hành)
Lab 1 — Guided: createUser()
Bước 0 — Vẽ diagram trước khi code. Với createUser("Alice"), hãy xác định:
- Environment nào được tạo?
- Binding nào là private state?
- Method nào đọc state?
- Method nào thay đổi state?
- Nếu gọi
createUser("Bob"), binding nào được tạo thêm?
Implement:
function createUser(initialName) {
// TODO
}
const user = createUser("Alice");
console.log(user.getName()); // Alice
user.rename("Alicia");
console.log(user.getName()); // AliciaYêu cầu:
namephải private.- Có
getName()vàrename(nextName). - Không lưu
namebằng property public nhưuser.name. - Hai user phải có state độc lập.
[Đáp án tham khảo]
function createUser(initialName) {
let name = initialName;
return {
getName() {
return name;
},
rename(nextName) {
name = nextName;
},
};
}- Giải thích: Mỗi invocation của
createUsertạo một bindingnameriêng.getNamevàrenamecủa cùng instance cùng tham chiếu đến binding đó.
Lab 2 — Partial Scaffold: createLogger()
Hoàn thiện factory sau:
function createLogger(prefix) {
let count = 0;
return {
log(message) {
// TODO: tăng count và log `[PREFIX] #COUNT message`
},
getCount() {
// TODO
},
};
}
const apiLogger = createLogger("API");
apiLogger.log("start"); // [API] #1 start
apiLogger.log("done"); // [API] #2 done
console.log(apiLogger.getCount()); // 2Sau đó tạo thêm:
const authLogger = createLogger("AUTH");
authLogger.log("login");
console.log(authLogger.getCount()); // 1
console.log(apiLogger.getCount()); // 2[Đáp án tham khảo]
function createLogger(prefix) {
let count = 0;
return {
log(message) {
count = count + 1;
console.log(`[${prefix}] #${count} ${message}`);
},
getCount() {
return count;
},
};
}- Giải thích:
prefixvàcountlà bindings của từng factory invocation. Mỗi logger có environment riêng nên prefix và counter không ảnh hưởng logger khác.
Lab 3 — Independent: createStore()
Implement một store nhỏ:
const store = createStore(10);
console.log(store.get()); // 10
store.set(20);
console.log(store.get()); // 20
store.update((value) => value + 5);
console.log(store.get()); // 25Contract:
get()
set(nextValue)
update(updater)Constraints:
statephải private.- Không dùng global mutable state.
- Không expose
statebằng property public. updatephải nhận state hiện tại và lưu kết quả mới.- Hai store phải giữ state độc lập.
[Đáp án tham khảo]
function createStore(initialState) {
let state = initialState;
return {
get() {
return state;
},
set(nextState) {
state = nextState;
},
update(updater) {
state = updater(state);
},
};
}- Giải thích:
get,setvàupdateđược tạo trong cùng factory invocation nên cùng truy cập một bindingstate. Một invocation khác tạo ra một bindingstatekhác.
9. Edge Cases (Các trường hợp ngoại lệ)
Edge Case 1 — Object khác nhau nhưng state vẫn shared
let count = 0;
function createCounter() {
return {
increment() {
count = count + 1;
},
getValue() {
return count;
},
};
}Phân tích
createCounter() vẫn trả về object mới mỗi lần gọi, nhưng count không nằm trong factory. Tất cả methods đều resolve cùng một binding bên ngoài. Đây là counterexample trực tiếp cho câu: "object mới thì state tự động độc lập."
Edge Case 2 — Binding private nhưng object reference bị expose
function createProfile() {
const profile = { name: "Alice" };
return {
getProfile() {
return profile;
},
};
}
const user = createProfile();
const profile = user.getProfile();
profile.name = "Changed";
console.log(user.getProfile().name);Phân tích
Binding profile vẫn không truy cập trực tiếp được từ bên ngoài, nhưng getProfile() trả ra chính object reference. Caller có thể mutate object đó. Closure làm binding private không đồng nghĩa với object được trả ra ngoài là immutable.
Edge Case 3 — Parameter binding là state của invocation
function createReader(value) {
return function () {
return value;
};
}
let source = 1;
const read = createReader(source);
source = 2;
console.log(read());Phân tích
Output là 1. Khi gọi createReader(source), giá trị hiện tại của source được truyền vào parameter binding value. Sau đó source và value là hai bindings khác nhau. Closure của read tham chiếu đến value, không tham chiếu đến biến source ở caller.
10. Debug Lab (Bài lab gỡ lỗi)
Debug Lab — Factory instances vô tình chia sẻ state
Symptom (Triệu chứng): Developer mong đợi a có giá trị 2 và b có giá trị 1, nhưng cả hai đều trả về 3.
Reproduction (Tái hiện lỗi):
let count = 0;
function createCounter() {
return {
increment() {
count = count + 1;
},
getValue() {
return count;
},
};
}
const a = createCounter();
const b = createCounter();
a.increment();
a.increment();
b.increment();
console.log(a.getValue()); // 3
console.log(b.getValue()); // 3Evidence (Bằng chứng):
createCounter()được gọi hai lần.- Hai object khác nhau được tạo.
- Nhưng chỉ có một declaration
let count = 0. - Declaration đó nằm ngoài
createCounter.
Hypothesis (Giả thuyết):
- Hai nhóm methods đều resolve cùng một outer binding
count. - Vì vậy update từ
avàbcộng dồn vào cùng state.
Verification (Xác minh): Tạo instance thứ ba và chỉ gọi một lần:
const c = createCounter();
c.increment();
console.log(a.getValue());
console.log(b.getValue());
console.log(c.getValue());Nếu cả ba cùng trả về 4, giả thuyết shared binding được xác nhận.
Root Cause (Nguyên nhân gốc rễ):
- State cần có ownership theo từng instance.
- Nhưng
countlại được khai báo ở scope dùng chung. - Factory tạo object mới, nhưng không tạo binding
countmới.
Fix (Sửa):
function createCounter() {
let count = 0;
return {
increment() {
count = count + 1;
},
getValue() {
return count;
},
};
}Prevention (Phòng ngừa):
- Khi review factory, xác định rõ mutable state nào là per-instance và state nào là shared.
- Với per-instance state, kiểm tra declaration có nằm trong factory invocation không.
- Test tối thiểu hai instance và interleave method calls.
- Không dùng "object khác nhau" làm bằng chứng rằng state đã độc lập.
11. Design Exercise (Bài tập thiết kế giải pháp)
Design Exercise — createWallet(initialBalance)
Bạn cần API:
getBalance()
deposit(amount)
withdraw(amount)Constraints:
balancephải private.deposit(amount)chỉ nhận số dương.withdraw(amount)không được làm balance âm.- Hai wallet phải độc lập.
- Caller không được sửa
balancetrực tiếp.
Trước khi code, hãy trả lời:
balancephải được khai báo ở scope nào?- Những method nào cần truy cập cùng binding
balance? - Validation nên nằm trong method hay để caller tự làm?
- Vì sao không nên trả trực tiếp
{ balance }rồi cho caller tự mutate? - Test nào chứng minh hai wallet không chia sẻ state?
[Một phương án tham khảo]
function createWallet(initialBalance) {
let balance = initialBalance;
return {
getBalance() {
return balance;
},
deposit(amount) {
if (amount <= 0) return false;
balance = balance + amount;
return true;
},
withdraw(amount) {
if (amount <= 0 || amount > balance) return false;
balance = balance - amount;
return true;
},
};
}- Decision:
balancelà per-instance state nên nằm trong factory. - Invariant: Mọi mutation đi qua
deposithoặcwithdraw, nơi contract được kiểm tra. - Isolation: Mỗi invocation tạo một binding
balanceriêng.
12. Production Scenario (Tình huống thực tế)
Production Scenario — Logger theo subsystem
Một ứng dụng tạo logger cho từng subsystem:
const authLogger = createLogger("AUTH");
const paymentLogger = createLogger("PAYMENT");Requirement:
- Mỗi logger có prefix riêng.
- Mỗi logger có counter riêng.
- Log của
AUTHkhông được làm counter củaPAYMENTtăng.
Implementation ban đầu:
function createLogger(prefix) {
let count = 0;
return {
log(message) {
count = count + 1;
console.log(`[${prefix}] #${count} ${message}`);
},
};
}Sau một refactor, developer chuyển count ra ngoài để "dùng chung":
let count = 0;
function createLogger(prefix) {
return {
log(message) {
count = count + 1;
console.log(`[${prefix}] #${count} ${message}`);
},
};
}Kết quả có thể thành:
[AUTH] #1 login
[PAYMENT] #2 create-order
[AUTH] #3 logoutVấn đề: Refactor không chỉ đổi vị trí variable. Nó đã đổi state ownership từ per-instance thành shared.
Engineering Decision: Trước khi kéo mutable state ra scope rộng hơn để "reuse", phải hỏi:
State này thuộc lifetime của từng instance hay thật sự phải được chia sẻ giữa mọi instance?
Đây là ví dụ một thay đổi scope nhỏ nhưng làm thay đổi semantics của hệ thống.
13. AI-assisted Exercise (Bài tập với AI)
Level C — Delegate & Inspect (Ủy quyền và kiểm tra)
- Tự trả lời trước: Viết 5–7 câu giải thích tại sao hai lần gọi
createStore(0)tạo được hai state độc lập. - Hỏi AI: Dùng prompt: "Explain why two instances created by the same JavaScript closure-based factory can hold independent state. Use invocation, lexical environment, binding, and closure."
- Inspect: Tìm xem AI có nói "closure copy value", "object tự có closure" hoặc "factory tự động tạo private state" không.
- Challenge: Đưa cho AI counterexample có state nằm ngoài factory và hỏi tại sao hai object lại share state.
- Verify: Kiểm tra lại mental model bằng knowledge từ Closure Formation, Lexical Environment và Variable Resolution.
Gợi ý
Câu trả lời tốt phải nối được chuỗi: factory invocation → environment mới → local binding mới → returned methods giữ reference → instance isolation.
Nếu AI chỉ nói "mỗi object có state riêng" mà không giải thích binding nằm ở đâu, câu trả lời chưa đủ depth.
Đáp án tham khảo
Bạn nghĩ:
- Mỗi lần gọi
createStorelà một invocation riêng. - Mỗi invocation tạo local binding
stateriêng. - Các methods được tạo trong invocation đó giữ reference cần thiết đến lexical environment tương ứng.
- Vì vậy methods của store A resolve
state_A, còn methods của store B resolvestate_B.
- Mỗi lần gọi
AI có thể trả lời:
- "Factory tạo ra object mới và closure lưu một copy của state trong object đó."
Điểm cần challenge:
- Object mới không đảm bảo state độc lập nếu state nằm ngoài factory.
- Closure không nên được mô tả là "copy state".
- State nằm trong lexical environment, không phải tự động "nằm trong object" chỉ vì object chứa methods.
Counterexample để kiểm tra:
let state = 0;
function createStore() {
return {
get: () => state,
set: (value) => {
state = value;
},
};
}- Kết luận:
- Mental model đúng phải giải thích được cả happy path lẫn counterexample shared state.
14. Teach Back (Dạy lại)
Giải thích cho một đồng nghiệp junior trong 2 phút:
"Tại sao
const a = createStore(0)vàconst b = createStore(0)có thể giữ hai state độc lập dù cả hai được tạo từ cùng một functioncreateStore?"
Yêu cầu:
- Dùng đúng terminology: invocation, environment, binding, closure.
- Không được trả lời chỉ bằng "vì factory tạo object mới".
- Phải giải thích điều gì xảy ra nếu
stateđược chuyển ra ngoài factory.
Mô phỏng
- Bạn nói:
- Khi
createStore(0)được gọi lần đầu, đó là một invocation riêng và nó tạo một lexical environment chứa bindingstate. - Các method như
getvàsetđược tạo trong invocation đó nên giữ reference cần thiết đến environment này. - Khi gọi
createStore(0)lần thứ hai, engine không dùng lại local bindingstatecủa lần đầu. Một invocation mới tạo một environment mới và một bindingstatemới. - Vì vậy methods của
atruy cập state của invocation A, còn methods củabtruy cập state của invocation B. - Object mới chỉ là public API được trả về. Nguyên nhân cốt lõi của state isolation là local bindings thuộc hai factory invocation khác nhau.
- Nếu đưa
statera ngoài factory, các methods của cảavàbcó thể resolve cùng một outer binding và bắt đầu ảnh hưởng lẫn nhau.
- Khi
💡 Hình dung factory là một khuôn. Mỗi lần gọi khuôn tạo một ngăn state mới và các methods được trả ra giữ chìa khóa đến đúng ngăn của lần gọi đó.
Gợi ý đánh giá bản thân
- Bạn có vẽ được hai environment song song cho
avàbkhông? - Bạn có chỉ ra được binding nào thuộc invocation nào không?
- Nếu di chuyển declaration
state, bạn có dự đoán được sharing behavior thay đổi ra sao không? - Bạn có giải thích được tại sao "object mới" không phải bằng chứng đủ cho "state mới" không?
15. Assessment (Đánh giá)
| Objective (Mục tiêu) | Hình thức đánh giá |
|---|---|
| Giải thích per-instance state | Teach Back — đúng invocation → environment → binding |
| Vẽ factory environment diagram | Worked Example + Lab 1 — hai environment độc lập |
| Dự đoán behavior của factory | Prediction — 3/3 đúng |
| Implement closure-based factory | Lab 1–3 — createUser, createLogger, createStore |
| Nhận biết per-instance vs shared state | Edge Case 1 + Prediction 2 |
| Debug shared-state bug | Debug Lab — xác định đúng root cause |
| Thiết kế API giữ private state và invariant | Design Exercise — createWallet |
16. Exit Criteria (Tiêu chí qua bài)
- [ ] Có thể giải thích tại sao hai lần gọi cùng một factory tạo được hai environment khác nhau.
- [ ] Có thể vẽ diagram cho hai factory instances và chỉ đúng bindings của từng instance.
- [ ] Dự đoán đúng 3/3 prediction scenarios.
- [ ] Implement được
createUser(),createLogger()vàcreateStore()bằng closure. - [ ] Chứng minh được hai instance không share state bằng một test cụ thể.
- [ ] Debug được shared-state bug bằng environment model.
- [ ] Phân biệt được "object mới" với "local state mới".
- [ ] Biết state declaration phải nằm ở đâu khi requirement là per-instance.
- [ ] Thiết kế được factory nhỏ giữ private state và bảo vệ invariant.
17. Spiral Connection (Liên kết xoắn ốc)
Previous (Trước): Closure Formation → Closure Lifetime → Private State. Bạn đã biết function có thể giữ outer environment sau khi outer return và dùng cơ chế đó để che giấu state.
Current (Hiện tại): Factory Functions. Bạn mở rộng từ một private state sang nhiều factory invocation, mỗi invocation tạo một environment riêng và một nhóm methods cùng truy cập state của instance đó.
Next (Tiếp theo):
- Lesson 1.4.5 — Closure trong Loop (một binding hay binding riêng cho từng iteration?)
- Lesson 1.4.6 — Closure + Callback (function được truyền đi nhưng vẫn giữ lexical environment)
- Stage 3 — Async JavaScript (closure sống qua thời gian và scheduling)
- Stage 8 — React Hooks (stale closure)
- Stage 11 — Memory & Debugging (retained references)