تزریق وابستگی در Node.js: چرا باید از آن استفاده کنیم و چگونه آن را انجام دهیم؟
ﺯﻣﺎﻥ ﻣﻄﺎﻟﻌﻪ: 9 دقیقه

تزریق وابستگی در Node.js: چرا باید از آن استفاده کنیم و چگونه آن را انجام دهیم؟

تزریق وابستگی (Dependency Injection) نوعی تکنیک کدنویسی است که در آن وابستگی‌ها توسط یک موجودیت خارجی (معمولا به عنوان پارامتر یا مرجع) وارد می‌شوند، به جای این ‌که در یک ماژول قرار بگیرند. این وابستگی‌ها اشیا یا سرویس‌هایی هستند که یک ماژول می‌تواند از آنها استفاده کند.

تزریق وابستگی یک مزیت بزرگ برای جاوا اسکریپت و بسیاری از زبان‌های دیگر است، زیرا این نوع تکنیک به ایجاد کامپوننت‌های مستقل، مقیاس‌پذیر و قابل استفاده مجدد کمک می‌کند. اما در توسعه وب با Node.js آنطور که باید شناخته شده نیست.

بسیاری از افراد می‌گویند که تزریق وابستگی در NodeJS امری غیرضروری است، زیرا می‌توانید وابستگی‌ها را با استفاده از دستور require به یک ماژول وارد نمایید. به علاوه چنین رویکردی کد پیچیده‌تری را برای نگهداری و تست ایجاد می‌کند. با این حال تزریق وابستگی NodeJS این مشکل را برطرف می‌سازد.

چرا باید از تزریق وابستگی استفاده کنیم؟

تزریق وابستگی جاوا اسکریپت چهار مزیت اساسی دارد:

1. تعمیر و نگهداری آسان

از آنجایی که این تکنیک شی و وابستگی آن را جدا می‌کند، کد قابل استفاده مجدد شده و پیکربندی آن نیز آسان‌تر می‌شود. تصور کنید سه کامپوننت X،  Yو Z وجود دارد. به طوری که Z به Y وابسته بوده و Y نیز به Z وابسته است. در صورتی که بخواهید چیزی به Y یا Z اضافه کنید، چه می‌شود؟ اگر از تزریق وابستگی استفاده نکنید، باید در X کدنویسی و ریفکتورینگ اضافی زیادی انجام دهید. اما اگر از تزریق وابستگی استفاده کنید، اضافه کردن هر چیز دیگری بسیار ساده‌تر می‌شود. ضمن این‌که X فقط به Y اهمیت می‌دهد، نه به Z یا هر چیز دیگری که به آن وابسته است.

2. تست ساده

انجام تست واحد بسیار ساده‌تر می‌شود. به طوری که اگر Y مستقیما به X ارجاع داده شده باشد، نمی‌توانید X را بدون تست Y آزمایش کنید. از طرفی اگر Y در X تزریق شود، می‌توانید یک Y ساختگی برای تست X ایجاد نمایید. برای مثال اگر Y یک پاسخ وب سرویس باشد، می‌توانید آن را با یک فایل کدنویسی شده که برای اهداف آزمایشی کاملا کار می‌کند، جایگزین کنید.

3. مدیریت بهتر

وقتی یک تیم بزرگ روی یک پروژه مشغول به کار است، دیر یا زود وظایف آن‌ها با هم تداخل پیدا می‌کند. به کمک تزریق وابستگی اعضای تیم کامپوننت‌های خود را جدا کرده و کمتر به کار دیگران وابسته می‌شوند. بنابراین کار روی چند قسمت می‌تواند به طور همزمان انجام شود و خطاهای احتمالی کمتر اتفاق می‌افتد.

4. مسئولیت واحد

مسئولیت واحد یا تک مسئولیتی اولین اصل از پنج اصل طراحی شی‌گرا است که اصول SOLID نامیده می‌شود. به این معنی که هر کامپوننت باید یک کار انجام دهد و مسئول یک چیز باشد. در غیر این صورت اگر کامپوننت شما دو وظیفه را بر عهده بگیرد، باید هر دو سناریو را با هر تغییر در کد تست کنید. بنابراین تزریق وابستگی جاوا اسکریپت راهی برای اجرای اصل مسئولیت واحد است.

چگونه تزریق وابستگی را انجام دهیم؟

روش‌های مختلفی برای اجرای تزریق وابستگی وجود دارد. برای درک ماهیت این تکنیک می‌توانید نمونه‌های اولیه زیادی را در وب بیابید. در همین حال من می‌خواهم به شما نشان دهم که چگونه یک تزریق وابستگی را در یک کد واقعی با کمک فریمورک Frida اجرا کنید.

فریمورک Frida

فریدا نمونه‌ای از تزریق کد برای Node.js دارد، اما به نظر می‌رسد که کمی قدیمی است و می‌تواند فقط با یک نسخه خاص Node.js کار کند. این فریمورک از کد تعبیه شده V8 برای اجرای یک رشته جاوا اسکریپت بهره می‌گیرد. من کد را به Node.js v8.16.0 x64 به روز کرده‌ام، اما جزئیات تزریق را در اینجا توضیح نمی‌دهم. خودتان به راحتی می‌توانید از طریق این لینک اطلاعات بیشتری کسب کنید، بنابراین به جای آن اجازه دهید به کد تزریق شده بپردازیم. در ابتدا بیایید ببینیم از کدام کد فریدا استفاده خواهیم کرد (جزئیات بیشتر را در اینجا می‌توانید دریافت نمایید).

  1. Module.findExportByName تابع اکسپورت شده C را از طریق نام آن پیدا می‌کند. به طور خلاصه وقتی کدهای کامپایل شده ++C را بررسی می‌کنیم، می‌توانیم نام‌های زیادی مانند ?[email protected]@[email protected]@[email protected] را ببینیم. اولین نکته این است که همه اسامی تابع مورد نیاز را در یک disassembler مانند IDA یا OllyDbg پیدا کنیم و آدرس حافظه آن را به دست آوریم.
  2. NativeFunction(POINTER, RETURN_VALUE, ARGUMENTS) برای تعریف اتصالات داخلی جاوا اسکریپت یا به اصطلاح binding یک تابع بومی استفاده می‌شود. در اینجا به منظور فراخوانی این توابع بعدا باید تعداد آرگومان‌ها و مقادیر بازگشتی صحیح را تعریف کنیم.
  3. NativeCallback کدی است که پس از فراخوانی تابع بومی اجرا می‌شود.
  4. WeakRef.bind برای نظارت بر یک اشاره‌گر خاص و فراخوانی بازگشتی در هنگام بازیافت حافظه استفاده می‌شود.
  5. Memory.alloc(N) n بایت را در حافظه اختصاص می‌دهد و اشاره‌گر را برای این بخش از حافظه برمی‌گرداند.

مفاهیم و توابع مورد استفاده

در اینجا می‌خواهم مفاهیمی را که قرار است از آن‌ها استفاده کنیم را توضیح دهم:

  • Isolate بیانگر نمونه‌ای جدا شده از موتور V8 است. Isolate‌های V8 استیت‌های کاملا مجزا دارند. مثلا شی‌های یک Isolate را نمی‌توان در Isolate دیگر استفاده کرد. هنگامی که V8 مقداردهی اولیه می‌شود، یک Isolate پیش‌فرض به طور ضمنی ایجاد و وارد می‌گردد. Embedder می‌تواند Isolateهای اضافی ایجاد کند و از آن‌ها به صورت موازی در چندین رشته استفاده نماید. یک Isolate را می‌توان حداکثر با یک رشته در هر نقطه از زمان وارد کرد. برای همگام‌سازی نیز باید از Locker/Unlocker API استفاده شود. به طور خلاصه این نوعی سندباکس بوده که شامل stateهای خاص خود است.
  • Context شامل یک زمینه اجرای سندباکس با مجموعه‌ای از اشیاء و توابع داخلی است.
  • HandleScope یک کلاس تخصیص یافته است که تعدادی از handle‌های محلی را مدیریت می‌کند. پس از این‌که محدوده handle مشخص شد، تمام handleهای محلی در آن محدوده تخصیص داده می‌شود تا زمانی که محدوده حذف شده یا محدوده دیگری ایجاد شود. اگر از قبل یک محدوده وجود داشته باشد و یک مورد جدید ایجاد شود، همه تخصیص‌ها در محدوده جدید انجام می‌شود تا زمانی که handle حذف گردد. پس از آن، handleهای جدید دوباره در محدوده اصلی تخصیص داده می‌شوند. پس از حذف محدوده یک handle محلی، بازیافت‌کننده حافظه دیگر شی ذخیره شده در handle را ردیابی نمی‌کند و ممکن است آن را جابه‌جا نماید. چرا که دسترسی به handle که محدوده برای آن حذف شده، تعریف نشده است.
  • Script یک اسکریپت جاوا اسکریپت کامپایل شده است.
  • String یک مقدار رشته جاوا اسکریپت است.
  • Value یک سوپرکلاس از تمام مقادیر و اشیاء جاوا اسکریپت است.

در مرحله بعد، توابع کتابخانه uv که از آن‌ها استفاده خواهیم کرد را توضیح می‌دهم. این کتابخانه امکان استفاده از سبک برنامه نویسی غیرهمگام و رویداد محور را می‌دهد:

  1. uv_async_init هندل را مقداردهی اولیه کرده و فراخوانی بازگشتی که در حلقه رویداد اجرا می‌شود را مشخص می‌کند.
  2. uv_default_loop حلقه رویداد پیش‌فرض را می‌گیرد.
  3. uv_async_send پاسخ را فراخوانی می‌کند.
  4. uv_unref شی ایجاد شده را از بین می‌برد.
  5. uv_close هندل‌های بسته شده را درخواست می‌کند.

همچنین از برخی توابع V8 نیز استفاده خواهیم کرد. به خصوص:

  1. V8::Isolate::GetCurrent(v8_Isolate_GetCurrent) نمونه ای از Isolate فعلی را می‌گیرد.
  2. V8::Isolate::GetCurrentContext(v8_Isolate_GetCurrentContext) کانتکست را از Isolate فعلی دریافت می‌کند.
  3. V8::Context::Enter(v8_Context_Enter) کانتکست را وارد می‌کند. پس از وارد کردن آن، تمام کدهای کامپایل شده و اجرا شده در این کانتکست کامپایل و اجرا می‌شوند. اگر کانتکست دیگری از قبل وارد شده باشد، کانتکست قدیمی ذخیره می‌شود تا با خروج از کانتکست جدید، بتوان آن را بازیابی کرد.
  4. V8::HandleScope::Init(v8_HandleScope_init) محدوده handle را مقداردهی اولیه می‌کند.
  5. V8::String::NewFromUtf8(v8_String_NewFromUtf8) رشته جاوا اسکریپت را از * const char دریافت می‌کند.
  6. V8::Script::Compile(v8_Script_Compile) اسکریپت مشخص شده مرتبط با کانتکست فعلی را کامپایل می‌کند.
  7. V8::Script::Run(v8_Script_Run) اسکریپت را اجرا می‌کند و مقدار به دست آمده را برمی‌گرداند. اگر اسکریپت مستقل از کانتکست باشد (یعنی با استفاده از ::New ایجاد شده باشد)، در کانتکست وارد شده فعلی اجرا می‌شود. اما اگر برای یک کانتکست خاص باشد (یعنی با استفاده از ::Compile ایجاد شده باشد)، در کانتکستی که در آن کامپایل شده است اجرا می‌گردد.
  8. v8::Value::Int32Value(v8_Value_Int32Value)، C int را از مقادیر جاوا اسکریپت دریافت می‌کند. Value برای انواع مختلف داده، توابع مخصوص به خود دارد.

لیست کدها

بیایید فهرست کدها را مرحله به مرحله مرور کنیم:

createFunc یک متد helper است که برای ایجاد JS bind با امضای مشخص به منظور استفاده مجدد به کار می‌رود.

try {
 const createFunc = (name, retval, args) => {
   const _ptr = Module.findExportByName(null, name);
   return new NativeFunction(_ptr, retval, args);
 }

همه فراخوانی‌هایی مانند const uv_default_loop = createFunc('uv_default_loop', 'pointer', []) برای تعریف JS bind به توابع بومی مورد نیاز هستند.

const uv_default_loop = createFunc('uv_default_loop', 'pointer', []);
const uv_async_init = createFunc('uv_async_init', 'int', ['pointer', 'pointer', 'pointer']);
const uv_async_send = createFunc('uv_async_send', 'int', ['pointer']);
const uv_unref = createFunc('uv_unref', 'void', ['pointer']);
const v8_Isolate_GetCurrent = createFunc('[email protected]@[email protected]@[email protected]', 'pointer', []);
const v8_Isolate_GetCurrentContext = createFunc('[email protected]@[email protected]@[email protected]@[email protected]@@[email protected]', 'pointer', ['pointer', 'pointer']);
const v8_Context_Enter = createFunc('[email protected]@[email protected]@QEAAXXZ', 'pointer', ['pointer']);
const v8_HandleScope_init = createFunc('[email protected]@@[email protected]@[email protected]@Z', 'void', ['pointer', 'pointer']);
const v8_String_NewFromUtf8 = createFunc('[email protected]@[email protected]@[email protected]@[email protected]@@[email protected]@[email protected]@[email protected]@Z', 'pointer', ['pointer', 'pointer', 'pointer', 'int', 'int']);
const v8_Script_Compile = createFunc('[email protected]@[email protected]@[email protected]@[email protected]@@[email protected][email protected]@[email protected]@@[email protected]@[email protected]@[email protected]@Z', 'pointer', ['pointer', 'pointer', 'pointer', 'pointer']);
const v8_Script_Run = createFunc('[email protected]@[email protected]@[email protected]@[email protected]@@[email protected]', 'pointer', ['pointer', 'pointer']);
const v8_Value_Int32Value = createFunc('[email protected]@[email protected]@QEBAHXZ', 'int64', ['pointer']);

scriptToExecute کد تزریق شده ماست.

const scriptToExecute = `((a, b)=>{
   console.log("Hello from Frida", a, b);
   return a+b;
 })(5, 17)`;

uv async handler را تعریف کرده، سپس processPending را به حلقه رویداد پیش‌فرض متصل می‌نماییم، بعد حلقه رویداد را برای فراخوانی بازگشتی آماده می‌کنیم. در داخل فراخوانی بازگشتی یک نمونه Isolate دریافت کرده، HandleScope جدید را مقداردهی اولیه می‌کنیم و کانتکست فعلی را می‌گیریم. در ادامه رشته جاوا اسکریپت را به رشته V8 تبدیل کرده، آن را کامپایل کرده و اجرا می‌کنیم. در نهایت فقط مقدار C int را از نتیجه اجرای اسکریپت استخراج می‌نماییم.

const processPending = new NativeCallback(function () {
   const isolate = v8_Isolate_GetCurrent();
   const scope = Memory.alloc(128);
   v8_HandleScope_init(scope, isolate);
   const opts = Memory.alloc(128);
   const context = v8_Isolate_GetCurrentContext(isolate, opts);
   const item = scriptToExecute;
   const unkMem = Memory.alloc(128);
   const source = v8_String_NewFromUtf8(unkMem, isolate, Memory.allocUtf8String(item), 0, -1);
   const script = v8_Script_Compile(context, Memory.readPointer(context), source, NULL);
   const result = v8_Script_Run(Memory.readPointer(context), context);
   const intResult = v8_Value_Int32Value(Memory.readPointer(result));
   console.log('Result', intResult);
}, 'void', ['pointer']);
const onClose = new NativeCallback(function () { }, 'void', ['pointer']);
const async = Memory.alloc(24);
uv_async_init(uv_default_loop(), async, processPending);
uv_async_send(async);
uv_unref(async);
}
catch (ex) {
console.log("Injected code execution error", ex);
}

جمع‌بندی

تزریق وابستگی Node.js آنطور که باید شناخته شده نیست. این کار برای ساده‌سازی و ایجاد یک تجربه کدنویسی بهتر با صرفه‌جویی در زمان طراحی شده است.

روش‌های مختلفی برای اجرای این تکنیک وجود دارد و روشی که در اینجا به شما معرفی کردم تنها یکی از آن‌هاست. خودتان می‌توانید نمونه‌های زیادی را در StackOverflow پیدا کنید.

با این حال صرف نظر از اینکه چگونه آن را پیاده‌سازی می‌کنید، مزایای آن شامل نگهداری آسان، تست ساده، مدیریت بهتر و مسئولیت واحد است.

منبع

چه امتیازی برای این مقاله میدهید؟

خیلی بد
بد
متوسط
خوب
عالی
در انتظار ثبت رای

1 ماه پیش
/@heshmati74
عرفان حشمتی
Full-Stack Web Developer

مهندس معماری سیستم های کامپیوتری، طراح و توسعه دهنده وب سایت

دیدگاه و پرسش

برای ارسال دیدگاه لازم است وارد شده یا ثبت‌نام کنید

ورود یا ثبت‌نام

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

عرفان حشمتی

Full-Stack Web Developer