วันนี้เราจะมาคุยกันเรื่องปัญหาที่หลายคนอาจจะเคยเจอ แต่อาจจะยังไม่รู้ว่ามันคืออะไร หรือแก้ยังไงดี นั่นก็คือ Circular Dependencies นั่นเอง
Circular Dependencies คืออะไร?
Circular Dependencies คือสถานการณ์ที่โมดูลหรือไฟล์สองตัวต้องพึ่งพาซึ่งกันและกัน เกิดเป็นวงจรการ import ที่วนไปวนมาไม่มีที่สิ้นสุด เช่น moduleA import moduleB และ moduleB ก็ import moduleA กลับ ซึ่งเป็นปัญหาที่เกิดขึ้นได้กับภาษาที่มีไอเดียของการแยก Module และการ Import แต่ว่าในบทความนี้จะขอใช้ตัวอย่างเป็น Node.js เพื่อความเข้าใจง่ายนะครับ
เป็นโค้ดหน้าตาแบบนี้
// index.js
const { getDataFromA } = require('./moduleA');
console.log('เริ่มต้นโปรแกรม');
const result = getDataFromA(); // จะเกิดปัญหา!
console.log('ผลลัพธ์:', result);
// moduleA.js
const { getDataFromB } = require('./moduleB');
function getDataFromA() {
console.log('Getting data from A');
return getDataFromB();
}
module.exports = { getDataFromA };
// moduleB.js
const { getDataFromA } = require('./moduleA');
function getDataFromB() {
console.log('Getting data from B');
return getDataFromA();
}
module.exports = { getDataFromB };
JavaScriptเมื่อเรารันโค้ดนี้ด้วยคำสั่ง node index.js จะเกิดปัญหาตามลำดับนี้:
- index.js เริ่มทำงานและพยายามโหลด moduleA
- moduleA พยายามโหลด moduleB เพื่อใช้ฟังก์ชัน getDataFromB
- moduleB พยายามโหลด moduleA กลับเพื่อใช้ฟังก์ชัน getDataFromA
- แต่ moduleA ยังโหลดไม่เสร็จ (กำลังรอ moduleB อยู่)
- เกิดการวนลูป และสุดท้าย getDataFromB จะได้ค่าเป็น undefined
- โปรแกรมอาจเกิด error หรือทำงานผิดพลาด
ออกมาเป็นผลลัพธ์แบบนี้:
เริ่มต้นโปรแกรม
Getting data from A
Getting data from B
D:\projects\node-test\moduleB.js:5
return getDataFromA();
^
TypeError: getDataFromA is not a function
at getDataFromB (D:\projects\node-test\moduleB.js:5:12)
at getDataFromA (D:\projects\node-test\moduleA.js:5:12)
at Object.<anonymous> (D:\projects\node-test\index.js:4:16)
at Module._compile (node:internal/modules/cjs/loader:1546:14)
at Object..js (node:internal/modules/cjs/loader:1689:10)
at Module.load (node:internal/modules/cjs/loader:1318:32)
at Function._load (node:internal/modules/cjs/loader:1128:12)
at TracingChannel.traceSync (node:diagnostics_channel:315:14)
at wrapModuleLoad (node:internal/modules/cjs/loader:218:24)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:170:5)
ShellScriptนี่เป็นตัวอย่างง่ายๆ ที่แสดงให้เห็นว่า Circular Dependencies สามารถทำให้โปรแกรมทำงานผิดพลาดได้อย่างไร ในโค้ดจริงที่ซับซ้อนกว่านี้ ปัญหาอาจซ่อนอยู่และตรวจจับได้ยากกว่านี้มาก
ทำไม Circular Dependencies ถึงเป็นปัญหาใหญ่?
- Runtime Errors: โค้ดอาจรันไม่ได้ หรือทำงานผิดพลาดเพราะได้ค่า undefined
- Debug ยาก: เมื่อเกิดปัญหา การตามหาต้นตอทำได้ยาก เพราะต้องไล่ดูการ import ที่วนไปวนมา
- ประสิทธิภาพแย่: Node.js ต้องพยายามแก้ปัญหาการโหลดโมดูลที่วนกัน ทำให้เสียทรัพยากรโดยไม่จำเป็น
- โค้ดซับซ้อนเกินไป: มักบ่งชี้ว่าการออกแบบโครงสร้างโค้ดอาจไม่ดีพอ
ที่ว่า Debug ยาก มันยากจริง ๆ นะ เพราะว่าในโค้ดจริง ๆ เราไม่ได้มีแค่ 2 ไฟล์ การ circular import ก็เลยไม่ได้เกิดแค่ระหว่างไฟล์ A และ B แบบในตัวอย่างข้างบน แต่ว่าอาจจะอาจ import กันต่อเนื่องยาว ๆ แล้วปลายสุดวนกลับไป import ตัวแรก
หรือแบบที่จริงยิ่งกว่านี้ก็คือ import กันไปมา
วิธีตามหา Circular Dependencies
เราสามารถใช้เครื่องมือ https://github.com/pahen/madge ช่วยในการตรวจสอบได้ โดย Madge เป็นเครื่องมือที่ช่วยให้เราสามารถมองเห็นความสัมพันธ์ระหว่างโมดูลในโปรเจคได้ง่ายขึ้น โดยจะสร้างแผนภาพแสดงการเชื่อมต่อระหว่างไฟล์ JavaScript และ CSS รวมถึงช่วยตรวจจับ circular dependencies ที่อาจก่อให้เกิดปัญหาในโค้ด
สามารถติดตั้งใช้งานได้ง่าย ๆ แบบนี้เลย
# ติดตั้ง madge
npm install -g madge
# ตรวจสอบ circular dependencies
madge --circular ./src/
# สร้าง dependency graph
madge --image dependency-graph.svg ./src/
Bashหน้าตารูปที่ Madge สร้างออกมาให้ก็จะหน้าตาประมาณนี้ ดูง่ายสุด ๆ
ภาพจาก pahen/madge: Create graphs from your CommonJS, AMD or ES6 module dependencies
ทางออกสำหรับปัญหา
1. แยก Shared Logic
การแยกโค้ดที่ใช้งานร่วมกันออกมาเป็นโมดูลหรือไฟล์ใหม่เป็นวิธีที่ทำได้ง่ายทึ่สุดแบบนึง แทนที่จะให้โมดูล A กับ B พึ่งพากันโดยตรง เราก็ย้ายฟังก์ชันหรือตัวแปรที่ใช้ร่วมกันไปไว้ในไฟล์ shared.js แล้วให้ทั้งสองโมดูลเรียกใช้จากที่เดียวกัน ทำให้ลดการพึ่งพาระหว่างโมดูลแล้วก็ไม่เกิด Circular Dependencies ด้วย
// shared.js
function processSharedData(data) {
// โค้ดที่ใช้ร่วมกัน
}
module.exports = { processSharedData };
// moduleA.js
const { processSharedData } = require('./shared');
function moduleALogic() {
// ใช้ processSharedData
}
// moduleB.js
const { processSharedData } = require('./shared');
function moduleBLogic() {
// ใช้ processSharedData
}
JavaScript2. ใช้ Event Emitter
Event Emitter เป็นวิธีที่ช่วยให้โมดูลต่างๆ สื่อสารกันได้โดยไม่ต้องรู้จักกันโดยตรง โดยใช้ระบบ publish-subscribe ผ่านตัวกลาง โมดูลนึงสามารถส่งข้อมูล (emit) และอีกโมดูลสามารถรับฟัง (listen) เหตุการณ์นั้นได้ ก็ไม่เกิดปัญหาแล้ว
// eventBus.js
const EventEmitter = require('events');
const eventBus = new EventEmitter();
module.exports = eventBus;
// moduleA.js
const eventBus = require('./eventBus');
function handleDataFromB(data) {
console.log('A received:', data);
}
eventBus.on('B_DATA', handleDataFromB);
// moduleB.js
const eventBus = require('./eventBus');
function sendDataToA() {
eventBus.emit('B_DATA', { some: 'data' });
}
JavaScript3. ปรับโครงสร้างโค้ดใหม่
สุดท้ายแล้วการเกิด Circular Dependencies ก็นับว่าเป็นปัญหาที่การออกแบบโค้ดของเรา การแก้ก็ตรงตัวเลยเหมือนกัน นั่นก็คือปรับโครงสร้างใหม่ อาจจะเป็นการเปลี่ยน logic ปรับหน้าตาหรือเปลี่ยนวิธีเก็บข้อมูล ซึ่งก็ขึ้นอยู่กับแต่ละงาน ต้องลองไปทบทวนโค้ดของตัวเองกันดูครับ
ใช้ NestJS ก็เจอได้เหมือนกัน
ปัญหานี้เป็นเรื่องที่เกิดขึ้นได้จากการออกแบบโค้ด เพราะฉะนั้นถึงจะใช้งาน Framework อย่าง NestJS ก็เจอปัญหานี้ได้ และน่าจะมีคนเจอกันอยู่จำนวนไม่น้อยเลยด้วย เพราะว่าใน Doc มีหัวข้อ Circular Dependencies อยู่ในหมวด Fundamentals เลย
ใน NestJS เราอาจไม่ต้องกังวลเรื่องที่เกิด Circular Dependencies โดยที่เราไม่รู้ตัว เพราะว่าจะมี Error ทำให้รันโค้ดไม่ได้ แจ้งเตือนทันที แล้วก็มีตัวช่วยอย่างการใช้ Forward reference มาแก้ปัญหาได้ แต่ว่าทางที่ดีก็ปรับโครงสร้างโค้ดให้ไม่เกิดเลยจะดีที่สุด หวังว่าจะช่วยให้ทุกคนได้รู้จักกับ Circular Dependencies แล้วก็สนุกกับการเขียนโค้ดได้มากยิ่งขึ้นนะครับ
แหล่งข้อมูล
- NestJS Circular Dependencies – Documentation | NestJS – A progressive Node.js framework
- Madge – pahen/madge: Create graphs from your CommonJS, AMD or ES6 module dependencies