Sau khi hoàn thành chương này, người đọc có khả năng:
Nhận diện 4 dạng Dynamic Connascence (Connascence động) trong mã nguồn.
Hiểu được tại sao lỗi sinh ra từ Dynamic Connascence thường khó tái hiện (Heisenbugs).
Áp dụng các kỹ thuật đóng gói (Encapsulation) để kiểm soát các trạng thái và luồng thực thi động.
Dynamic Connascence chỉ bộc lộ khi hệ thống thực sự chạy (runtime). Kể cả khi code của bạn vượt qua mọi bước kiểm tra type-check của TypeScript, vượt qua linting 100%, ứng dụng vẫn có thể crash nếu vi phạm các rào cản động này.
1. Connascence of Execution (CoE)
Khái niệm: Trật tự thực thi (thứ tự gọi hàm) của một số đoạn code là bắt buộc, nếu gọi sai thứ tự, hệ thống sẽ lỗi.
Ví dụ Problematic (CoE):
class EmailSender {
private toAddress: string;
private content: string;
setAddress(address: string) { this.toAddress = address; }
setContent(content: string) { this.content = content; }
send() {
if (!this.toAddress || !this.content) throw new Error("Chưa khởi tạo đủ");
// Gửi email...
}
}
// Chỗ sử dụng:
const sender = new EmailSender();
sender.setAddress("[email protected]");
sender.send(); // quên gọi setContent() -> crash time
Người sử dụng class này phải tự ghi nhớ trong đầu "thuật toán" cấu hình: phải gọi các hàm setter trước khi gọi send().
Cách hạ cấp: Loại bỏ hoàn toàn CoE bằng cách sử dụng Constructor bắt buộc, hoặc Factory/Builder patterns trả về một object đã hoàn thiện sẵn sàng để chạy.
class EmailSender {
// Yêu cầu truyền đủ thông tin ngay khi khởi tạo
constructor(private toAddress: string, private content: string) {}
send() { ... }
}
2. Connascence of Timing (CoTm)
Khái niệm: Hệ thống chỉ hoạt động đúng dựa trên thời gian hoặc tốc độ tương đối giữa các tiến trình / threads / thao tác bất đồng bộ. Đây là ngọn nguồn của Race Conditions.
Ví dụ Problematic (CoTm):
let configReady = false;
// Async function giả lập load config mất 100ms
setTimeout(() => { configReady = true; }, 100);
// Nơi khác cố gắng đọc config
setTimeout(() => {
if (configReady) {
console.log("Hệ thống khởi động thành công");
} else {
console.log("Lỗi: Không tìm thấy config");
}
}, 150);
Mã này chạy đúng trên máy local mạnh, nhưng khi đẩy lên server chịu tải cao, hàm load config mất 200ms, và logic đọc config ở 150ms sẽ bị lỗi tạch. Hai đoạn code này bị phụ thuộc vào timing của nhau.
Cách hạ cấp: Sử dụng Promises, Async/Await hoặc Event Emitters để đảm bảo thứ tự dữ liệu thay vì "đoán" dựa trên thời gian trễ.
3. Connascence of Value (CoV)
Khái niệm: Các thành phần chia sẻ chung một cấu trúc dữ liệu bị đột biến (mutable state). Sự thay đổi giá trị ở một nơi sẽ làm thay đổi trạng thái của hệ thống ở một nơi hoàn toàn khác.
Ví dụ Problematic (CoV):
const globalAppConfig = { theme: "light", retries: 3 };
function setupDatabase(config: any) {
// Thay đổi ngầm định global object
config.retries = 5;
}
function setupNetwork(config: any) {
// Nếu setupNetwork chạy sau, nó sẽ nhận giá trị retries là 5 thay vì 3
console.log(config.retries);
}
setupDatabase(globalAppConfig);
setupNetwork(globalAppConfig);
Các component phụ thuộc vào cùng một reference trên memory. Khi một người vô tình .push() vào một mảng hoặc thay đổi một object property, những component khác dùng chung mảng/object đó sẽ bị sai lệch logic.
Cách hạ cấp: Sử dụng dữ liệu bất biến (Immutability). Pass-by-value thay vì Pass-by-reference. Clone đối tượng trước khi xử lý (ví dụ: dùng spread operator {...config}).
4. Connascence of Identity (CoI)
Khái niệm: Hai hay nhiều thành phần bắt buộc phải dùng chung chính xác cùng một instance (bản thể) vật lý trong bộ nhớ mới có thể hoạt động.
Ví dụ Problematic (CoI):
Giả sử bạn có một Message Broker in-memory:
class EventBus {
listeners = [];
subscribe(fn) { this.listeners.push(fn); }
emit(data) { this.listeners.forEach(fn => fn(data)); }
}
// Module A
const busA = new EventBus();
busA.subscribe(data => console.log("Nhận: ", data));
// Module B
const busB = new EventBus();
busB.emit("Xin chào"); // Module A không nhận được gì cả.
Để Module A và B giao tiếp được với nhau, chúng bắt buộc phải import và sử dụng chung một instance duy nhất (Singleton) của EventBus. Nếu vì lý do cấu hình Dependency Injection sai, hệ thống tạo ra 2 instance khác nhau, mọi thứ sẽ sụp đổ.
Đánh giá: CoI không hẳn luôn luôn là lỗi, nhưng nó là một yêu cầu cực kỳ khắt khe lên kiến trúc (đặc biệt trong môi trường phân tán hoặc microservices).
5. Vì sao Dynamic Connascence nguy hiểm hơn Static Connascence
Trong thiết kế phần mềm, Dynamic Connascence được coi là một mã độc thầm lặng vì ba lý do chính:
Vô hình trước công cụ tĩnh: TypeScript hay các trình biên dịch mạnh mẽ khác không thể cảnh báo bạn nếu bạn gọi hàm sai thứ tự (CoE), cấu hình sai thời gian (CoTm) hay chia sẻ chung reference (CoV). Code vẫn biên dịch thành công 100%.
Blast Radius (Bán kính ảnh hưởng) khó xác định: Với Static Connascence (như CoN - Tên), khi đổi tên hàm, IDE sẽ vạch đỏ toàn bộ những file liên quan. Với Dynamic Connascence (như CoV - Giá trị dùng chung), bạn phải tự lần mò trong hàng chục nghìn dòng code xem ai đã ngầm chỉnh sửa object global đó.
Lỗi lúc có lúc không (Heisenbugs): Đặc biệt là CoE và CoTm, lỗi phụ thuộc vào trạng thái mạng, tốc độ ổ cứng tại thời điểm chạy ứng dụng. Nó có thể qua được QA ở môi trường Staging nhưng lại crash thảm khốc trên Production.
Nguyên tắc vàng: Khi thiết kế hoặc refactoring hệ thống, mục tiêu tối thượng không phải là đạt Connascence = 0 (điều đó là không thể), mà là luôn nỗ lực đẩy các Dynamic Connascence trở thành Static Connascence, và tiếp tục đẩy các Static Connascence mạnh xuống các cấp độ yếu hơn (Name, Type).