Skip to main content
0
Software Development

Circular Dependencies เมื่อโค้ดของเรา Import กันเป็นวงกลม

วันนี้เราจะมาคุยกันเรื่องปัญหาที่หลายคนอาจจะเคยเจอ แต่อาจจะยังไม่รู้ว่ามันคืออะไร หรือแก้ยังไงดี นั่นก็คือ Circular Dependencies นั่นเอง

Photo by Ugur Arpaci on Unsplash

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 จะเกิดปัญหาตามลำดับนี้:

  1. index.js เริ่มทำงานและพยายามโหลด moduleA
  2. moduleA พยายามโหลด moduleB เพื่อใช้ฟังก์ชัน getDataFromB
  3. moduleB พยายามโหลด moduleA กลับเพื่อใช้ฟังก์ชัน getDataFromA
  4. แต่ moduleA ยังโหลดไม่เสร็จ (กำลังรอ moduleB อยู่)
  5. เกิดการวนลูป และสุดท้าย getDataFromB จะได้ค่าเป็น undefined
  6. โปรแกรมอาจเกิด 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 ถึงเป็นปัญหาใหญ่?

  1. Runtime Errors: โค้ดอาจรันไม่ได้ หรือทำงานผิดพลาดเพราะได้ค่า undefined
  2. Debug ยาก: เมื่อเกิดปัญหา การตามหาต้นตอทำได้ยาก เพราะต้องไล่ดูการ import ที่วนไปวนมา
  3. ประสิทธิภาพแย่: Node.js ต้องพยายามแก้ปัญหาการโหลดโมดูลที่วนกัน ทำให้เสียทรัพยากรโดยไม่จำเป็น
  4. โค้ดซับซ้อนเกินไป: มักบ่งชี้ว่าการออกแบบโครงสร้างโค้ดอาจไม่ดีพอ

ที่ว่า 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
}
JavaScript

2. ใช้ 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' });
}
JavaScript

3. ปรับโครงสร้างโค้ดใหม่

สุดท้ายแล้วการเกิด Circular Dependencies ก็นับว่าเป็นปัญหาที่การออกแบบโค้ดของเรา การแก้ก็ตรงตัวเลยเหมือนกัน นั่นก็คือปรับโครงสร้างใหม่ อาจจะเป็นการเปลี่ยน logic ปรับหน้าตาหรือเปลี่ยนวิธีเก็บข้อมูล ซึ่งก็ขึ้นอยู่กับแต่ละงาน ต้องลองไปทบทวนโค้ดของตัวเองกันดูครับ

ใช้ NestJS ก็เจอได้เหมือนกัน

ปัญหานี้เป็นเรื่องที่เกิดขึ้นได้จากการออกแบบโค้ด เพราะฉะนั้นถึงจะใช้งาน Framework อย่าง NestJS ก็เจอปัญหานี้ได้ และน่าจะมีคนเจอกันอยู่จำนวนไม่น้อยเลยด้วย เพราะว่าใน Doc มีหัวข้อ Circular Dependencies อยู่ในหมวด Fundamentals เลย

ใน NestJS เราอาจไม่ต้องกังวลเรื่องที่เกิด Circular Dependencies โดยที่เราไม่รู้ตัว เพราะว่าจะมี Error ทำให้รันโค้ดไม่ได้ แจ้งเตือนทันที แล้วก็มีตัวช่วยอย่างการใช้ Forward reference มาแก้ปัญหาได้ แต่ว่าทางที่ดีก็ปรับโครงสร้างโค้ดให้ไม่เกิดเลยจะดีที่สุด หวังว่าจะช่วยให้ทุกคนได้รู้จักกับ Circular Dependencies แล้วก็สนุกกับการเขียนโค้ดได้มากยิ่งขึ้นนะครับ

แหล่งข้อมูล

Develeper

Author Develeper

More posts by Develeper

เราใช้คุกกี้เพื่อพัฒนาประสิทธิภาพ และประสบการณ์ที่ดีในการใช้เว็บไซต์ของคุณ คุณสามารถศึกษารายละเอียดได้ที่ นโยบายความเป็นส่วนตัว และสามารถจัดการความเป็นส่วนตัวเองได้ของคุณได้เองโดยคลิกที่ ตั้งค่า

ตั้งค่าความเป็นส่วนตัว

คุณสามารถเลือกการตั้งค่าคุกกี้โดยเปิด/ปิด คุกกี้ในแต่ละประเภทได้ตามความต้องการ ยกเว้น คุกกี้ที่จำเป็น

ยอมรับทั้งหมด
จัดการความเป็นส่วนตัว
  • คุกกี้ที่จำเป็น
    เปิดใช้งานตลอด

    ประเภทของคุกกี้มีความจำเป็นสำหรับการทำงานของเว็บไซต์ เพื่อให้คุณสามารถใช้ได้อย่างเป็นปกติ และเข้าชมเว็บไซต์ คุณไม่สามารถปิดการทำงานของคุกกี้นี้ในระบบเว็บไซต์ของเราได้
    รายละเอียดคุกกี้

  • คุกกี้สำหรับการติดตามทางการตลาด

    ประเภทของคุกกี้ที่มีความจำเป็นในการใช้งานเพื่อการวิเคราะห์ และ นำเสนอโปรโมชัน สินค้า รวมถึงหลักสูตรฟรี และ สิทธิพิเศษต่าง ๆ คุณสามารถเลือกปิดคุกกี้ประเภทนี้ได้โดยไม่ส่งผลต่อการทำงานหลัก เว้นแต่การนำเสนอโปรโมชันที่อาจไม่ตรงกับความต้องการ
    รายละเอียดคุกกี้

บันทึกการตั้งค่า