کانتینرهای تزریق وابستگی در جاوا اسکریپت — از صفر تا صد

آخرین به‌روزرسانی: ۶ اردیبهشت ۱۳۹۹
زمان مطالعه: ۹ دقیقه

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

تزریق وابستگی بدون کانتینر

در این بخش به مرور سریع ماهیت تزریق وابستگی، شیوه نمایش آن در کد، مشکلاتی که حل می‌کند و مشکلاتی که از آن‌ها رنج می‌برد خواهیم پرداخت. تزریق وابستگی الگویی است که به ما کمک می‌کند تا از هاردکد کردن وابستگی‌ها در ماژول‌ها جلوگیری کنیم و به فراخوانی کننده این امکان را بدهیم که آن‌ها را تغییر داده و در صورت نیاز موارد مورد نظر خود را ارائه کند. این وابستگی‌ها می‌توانند در مرحله سازنده (وهله‌سازی) تزریق شوند و یا در ادامه آن‌ها را با یک متد setter تزریق کنیم:

class Frog {
  constructor(name, gender) {
    this.name = name
    this.gender = gender
  }
  
  jump() {
    console.log('jumped')
  }
}

class Toad {
  constructor(habitat, name, gender) {
    this.habitat = habitat
    this.frog = new Frog(name, gender)
  }
}

const mikeTheToad = new Toad('land', 'mike', 'male')

این روش برخی مشکلات دارد که در ادامه آن‌ها را توضیح می‌دهیم.

مشکل شماره 1

اگر لازم باشد شیوه ساخته شدن Toad را تغییر دهیم و از چیزی شکننده مانند موقعیت‌یابی آرگومان‌ها یا ساختمان داده آن‌ها استفاده کنیم، باید کد را به صورت دستی تغییر دهیم، ‌زیرا در بلوک کد هاردکد شده است.

به عنوان مثال این سناریو در زمانی اتفاق می‌افتد که بخواهیم یک «تغییر ناسازگار» (breaking change) در کلاس ایجاد کنیم. اگر یک پارامتر سوم مانند weight به سازنده Frog اضافه کنیم، چنین اتفاقی رخ می‌دهد:

class Toad {
  constructor(habitat, name, gender, weight) {
    this.habitat = habitat
    this.frog = new Frog(name, gender, weight)
  }
}

در این صورت Toud باید به‌روزرسانی شود، زیرا این وابستگی جدید در مرحله وهله‌سازی Frog اضافه شده است:

class Toad {
  constructor(habitat, name, gender, weight) {
    this.habitat = habitat
    this.frog = new Frog(name, gender, weight)
  }
}

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

مشکل شماره 2

باید هر بار بدانیم کدام وابستگی برای Toad استفاده شده است. مشکل دیگر این رویکرد آن است که باید بدانیم Toad هم اکنون به چهار آرگومان دقیقاً به همان ترتیب که در وهله Frog مقداردهی شده است نیاز دارد و حتی باید انواع داده آن‌ها را نیز بدانیم، چون در غیر این صورت به آسانی باگ ایجاد می‌شود.

این وضعیت در صورتی که بدانیم Toad اساساً یک Frog است بغرنج‌تر می‌شود چون با دانستن این نکته ممکن است به صورت تصادفی تصور کنید که Toad باید Forg را بسط داده باشد.

از این رو درک می‌کنیم که به جای آن یک وهله از Frog درون Toad ایجاد شده است و اینک همه چیز با هم مخلوط می‌شود زیرا شما یک انسان هوشمند هستید و کدی که مقابل شما قرار دارد با دنیای واقعی همخوانی ندارد.

مشکل شماره 3

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

class Frog {
  constructor({ name, gender, weight }) {
    this.name = name
    this.gender = gender
    this.weight = weight
  }
  
  jump() {
    console.log('jumped')
  }
}

class Toad {
  constructor(habitat, frog) {
    this.habitat = habitat
    this.frog = frog
  }
}

این کد آسانی است. اکنون زمانی که تغییر ناسازگار دیگری در Frog رخ دهد، مثلاً آرگومان‌ها درون شیء جاوا اسکریپت قرار گیرند، دیگر حتی لازم نیست به Toad مراجعه کنیم یا وقت خود با خواندن Toad و سپس Frpg و سپس بازگشتن به Toad و همین طور تا آخر به هدر بدهیم.

دلیل این امر آن است که اینک می‌توانیم بخشی که یک وهله از Toad را ایجاد می‌کند تغییر دهیم. این وضعیت بسیار بهتر از این است که مجبور باشیم وارد قضایا شویم و کد مربوط به پیاده‌سازی Toad را تغییر دهیم که رویه بدی محسوب می‌شود.

دیگر لازم نیست در مورد شیوه ساخته شدن frog نگران باشد و باید تنها بداند که frog را به عنوان یک آرگومان می‌گیرد و آن را در مشخصه frog. برای استفاده‌های آتی ذخیره می‌کند. اکنون مسئولیت وابستگی‌های آن را بر عهده می‌گیرید:

const mikeTheToad = new Toad(
  'land',
  new Frog({
    name: 'mike',
    gender: 'male',
    weight: 12.5,
  }),
)

بنابراین برخی رویه‌های کدنویسی تمیز را با جدا کردن جزییات پیاده‌سازی Frog از سازنده Toad تمرین کرده‌ایم. این کار به این جهت مطلوب است که دیگر لازم نیست toad در مورد شیوه ساخته شدن Frog اطلاع داشته باشد و هر چه که باشد می‌تواند آن را بسط دهد.

الگوی کانتینر تزریق وابستگی (DIC)

اکنون که مفاهیم تزریق وابستگی را با هم مرور کردیم، نوبت آن رسیده که در مورد کانتینر تزریق وابستگی صحبت کنیم. سؤال این است که چرا به الگوی DIC نیاز داریم و چرا تزریق وابستگی بدون کانتینر در برخی سناریوهای پیچیده به تنهایی کافی نیست؟

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

class Frog {
  constructor({ name, gender, weight }) {
    this.name = name
    this.gender = gender
    this.weight = weight
  }
  
  jump() {
    console.log('jumped')
  }
  
  setHabitat(habitat) {
    this.habitat = habitat
  }
}

class Toad extends Frog {
  constructor(options) {
    super(options)
  }
  
  leap() {
    console.log('leaped')
  }
}

class Person {
  constructor() {
    this.id = createId()
  }
  
  setName(name) {
    this.name = name
    return this
  }
  
  setGender(gender) {
    this.gender = gender
    return this
  }
  
  setAge(age) {
    this.age = age
    return this
  }
}

function createId() {
  var idStrLen = 32
  var idStr = (Math.floor(Math.random() * 25) + 10).toString(36) + '_'
  idStr += new Date().getTime().toString(36) + '_'
  do {
    idStr += Math.floor(Math.random() * 35).toString(36)
  } while (idStr.length < idStrLen)
  return idStr
}

class FrogAdoptionFacility {
  constructor(name, description, location) {
    this.name = name
    this.description = description
    this.location = location
    this.contracts = {}
    this.adoptions = {}
  }
  
  createContract(employee, client) {
    const contractId = createId()
    this.contracts[contractId] = {
      id: contractId,
      preparer: employee,
      client,
      signed: false,
    }
    return this.contracts[contractId]
  }
  
  signContract(id, signee) {
    this.contracts[id].signed = true
  }
  
  setAdoption(frogOwner, frogOwnerLicense, frog, contract) {
    const adoption = {
      [frogOwner.id]: {
        owner: {
          firstName: frogOwner.owner.name.split(' ')[0],
          lastName: frogOwner.owner.name.split(' ')[1],
          id: frogOwner.id,
        },
        frog,
        contract,
        license: {
          id: frogOwnerLicense.id,
        },
      },
    }
    this.adoptions[contract.id] = adoption
  }
  
  getAdoption(id) {
    return this.adoptions[id]
  }
}

class FrogParadiseLicense {
  constructor(frogOwner, licensePreparer, frog, location) {
    this.id = createId()
    this.client = {
      firstName: frogOwner.name.split(' ')[0],
      lastName: frogOwner.name.split(' ')[1],
      id: frogOwner.id,
    }
    this.preparer = {
      firstName: licensePreparer.name.split(' ')[0],
      lastName: licensePreparer.name.split(' ')[1],
      id: licensePreparer.id,
    }
    this.frog = frog
    this.location = `${location.street} ${location.city} ${location.state} ${location.zip}`
  }
}

class FrogParadiseOwner {
  constructor(frogOwner, frogOwnerLicense, frog) {
    this.id = createId()
    this.owner = {
      id: frogOwner.id,
      firstName: frogOwner.name.split(' ')[0],
      lastName: frogOwner.name.split(' ')[1],
    }
    this.license = frogOwnerLicense
    this.frog = frog
  }
  
  createDocument() {
    return JSON.stringify(this, null, 2)
  }
}

کل فرایند adoption زمانی که setAdoption از FrogAdoptionFacility فراخوانی می‌شود، پایان می‌یابد. بیاید فرض کنیم که شروع به توسعه کد با استفاده از این کلاس‌ها کرده‌ایم و در نهایت به نسخه‌ای مانند زیر می‌رسیم:

const facilityTitle = 'Frog Paradise'

const facilityDescription =
  'Your new one-stop location for fresh frogs from the sea! ' +
  'Our frogs are housed with great care from the best professionals all over the world. ' +
  'Our frogs make great companionship from a wide variety of age groups, from toddlers to ' +
  'senior adults! What are you waiting for? ' +
  'Buy a frog today and begin an unforgettable adventure with a companion you dreamed for!'

const facilityLocation = {
  address: '1104 Bodger St',
  suite: '#203',
  state: 'NY',
  country: 'USA',
  zip: 92804,
}

const frogParadise = new FrogAdoptionFacility(
  facilityTitle,
  facilityDescription,
  facilityLocation,
)

const mikeTheToad = new Toad({
  name: 'mike',
  gender: 'male',
  weight: 12.5,
})

const sally = new Person()
sally
  .setName('sally tran')
  .setGender('female')
  .setAge(27)
  
const richardTheEmployee = new Person()
richardTheEmployee
  .setName('richard rodriguez')
  .setGender('male')
  .setAge(77)
  
const contract = frogParadise.createContract(richardTheEmployee, sally)
frogParadise.signContract(contract.id, sally)

const sallysLicense = new FrogParadiseLicense(
  sally,
  richardTheEmployee,
  mikeTheToad,
  facilityLocation,
)

const sallyAsPetOwner = new FrogParadiseOwner(sally, sallysLicense, mikeTheToad)
frogParadise.setAdoption(sallyAsPetOwner, sallysLicense, mikeTheToad, contract)

const adoption = frogParadise.getAdoption(contract.id)
console.log(JSON.stringify(adoption, null, 2))

اگر این کد را اجرا کنیم، کار می‌کند و یک شیء adoption می‌سازد که به صورت زیر است:

class FrogParadiseOwner {
  constructor(frogOwner, frogOwnerLicense, frog) {
    this.id = createId()
    this.owner = frogOwner
    this.license = frogOwnerLicense
    this.frog = frog
  }
  
  createDocument() {
    return JSON.stringify(this, null, 2)
  }
}

اینک یک اپلیکیشن کاملاً زیبا داریم که یک واگذاری frog را انجام می‌دهد و مشتریان می‌توانند دران یک frog به دست آورند. اما فرایند adoption صرفاً یک تراکنش پولی دادن/گرفتن ساده نیست.

فرض می‌کنیم که قانونی وجود دارد که الزام می‌کند این فرایند برای هر adoption یک FROG به مالکان جدید باید اجرا شود. بنابراین لازم است که قراردادی ایجاد شده و امضای مشتری در آن وارد شود. در ادامه یک پروانه نیز ایجاد می‌شود که مشتریان باید برای حفاظت حقوقی خود داشته باشند. در نهایت adoption کامل می‌شود.

به کلاس FrogOwner زیر توجه کنید:

class FrogParadiseOwner {
  constructor(frogOwner, frogOwnerLicense, frog) {
    this.id = createId()
    this.owner = frogOwner
    this.license = frogOwnerLicense
    this.frog = frog
  }
  
  createDocument() {
    return JSON.stringify(this, null, 2)
  }
}

این کلاس سه وابستگی به صورت frogOwner، frogOwnerLicense و frog دارد. فرض کنید یک به‌روزرسانی در مورد frogOwner وجود دارد و به وهله‌ای از Client تبدیل می‌شود:

class Client extends Person {
  
  setName(name) {
    this.name = name
  }
}

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

این همان جایی است که کانتینر تزریق وابستگی می‌تواند موجب بهبود شود، زیرا در این حالت تنها باید کد در یک موقعیت تغییر یابد. بنابراین کانتینر تزریق وابستگی به صورت زیر خواهد بود:

import parseFunction from 'parse-function'

const app = parseFunction({
  ecmaVersion: 2017,
})

class DIC {
  constructor() {
    this.dependencies = {}
    this.factories = {}
  }
  
  register(name, dependency) {
    this.dependencies[name] = dependency
  }
  
  factory(name, factory) {
    this.factories[name] = factory
  }
  
  get(name) {
    if (!this.dependencies[name]) {
      const factory = this.factories[name]
      if (factory) {
        this.dependencies[name] = this.inject(factory)
      } else {
        throw new Error('No module found for: ' + name)
      }
    }
    return this.dependencies[name]
  }
  
  inject(factory) {
    const fnArgs = app.parse(factory).args.map((arg) => this.get(arg))
    return new factory(...fnArgs)
  }
}

اینک به جای مقداردهی مستقیم آن مانند قبل و الزام به تغییر دادن همه وهله‌های دیگر کد:

class Client extends Person {
  setName(name) {
    this.name = name
  }
}

const dic = new DIC()
dic.register('frogOwner', Client)
dic.register('frogOwnerLicense', sallysLicense)
dic.register('frog', mikeTheToad)
dic.factory('frog-owner', FrogParadiseOwner)

const frogOwner = dic.get('frog-owner')

می‌توانیم از DIC برای یک‌بار به‌روزرسانی آن اقدام کنیم و دیگر نیازی به تغییر همه بخش‌های کد نداریم، زیرا جهت‌گیری گردش برای آن کانتینر معکوس شده است:

const frogOwner = new FrogParadiseOwner(Client, sallysLicense, mikeTheToad)
// some other location
const frogOwner2 = new FrogParadiseOwner(...)
// some other location
const frogOwner3 = new FrogParadiseOwner(...)
// some other location
const frogOwner4 = new FrogParadiseOwner(...)
// some other location
const frogOwner5 = new FrogParadiseOwner(...)

اینک کاری که DIC انجام می‌دهد را توضیح می‌دهیم. ما کلاس‌ها یا تابع‌هایی را که می‌خواهیم از سوی DIC حل شوند را با ارسال به متد.factory() درج می‌کنیم که در نهایت در مشخصه.factory ذخیره می‌شوند:

کانتینرهای تزریق وابستگی در جاوا اسکریپت

در مورد هر کدام از این تابع‌ها که به ()factory. ارسال می‌شوند باید آرگومان‌هایشان با استفاده از ()register ثبت شوند تا بتوان در زمان مقداردهی تابع درخواستی از سوی کانتینر مورد استفاده قرار گیرند. این آرگومان‌ها از مشخصه dependencies. دریافت می‌شوند. با استفاده از متد ()dependencies. می‌توانید چیزهایی به وابستگی‌ها اضافه کنید:

کانتینرهای تزریق وابستگی در جاوا اسکریپت

زمانی که بخواهید چیزی را بازیابی کنید، می‌توانید از get. به همراه نوعی key استفاده کنید. این متد از KEY برای گشتن به دنبال dependencies استفاده می‌کند و در صورتی که چیزی پیدا کند آن را بازگشت می‌دهد. در غیر این صورت به دنبال factories می‌گردد و در صورتی که چیزی پیدا کند با آن مانند یک تابع رفتار می‌کند که باید resolve شود.

کانتینرهای تزریق وابستگی در جاوا اسکریپت

سپس اجزا را به inject. می‌سپارد که نام‌های وابستگی‌های (آرگومان‌ها را خوانده و آن‌ها را از مشخصه dependencies. می‌گیرد و تابع را اجرا کرده و آرگومان‌ها را تزریق می‌کند و نتیجه را بازگشت می‌دهد.

کانتینرهای تزریق وابستگی در جاوا اسکریپت

ما در مثال کدمان، از parse-function استفاده کرده‌‌ایم تا به متد inject امکان بدهیم که نام‌های آرگومان یک تابع را به دست آورد. برای این که این کار را بدون کتابخانه انجام دهیم، می‌توانیم آرگومان‌های دیگری به get. اضافه کنیم کرده و به صورت زیر به inject. آن ارسال کنیم:

class DIC {
  constructor() {
    this.dependencies = {}
    this.factories = {}
  }
  
  register(name, dependency) {
    this.dependencies[name] = dependency
  }
  
  factory(name, factory) {
    this.factories[name] = factory
  }
  
  get(name, args) {
    if (!this.dependencies[name]) {
      const factory = this.factories[name]
      if (factory) {
        this.dependencies[name] = this.inject(factory, args)
      } else {
        throw new Error('No module found for: ' + name)
      }
    }
    return this.dependencies[name]
  }
  
  inject(factory, args = []) {
    const fnArgs = args.map((arg) => this.get(arg))
    return new factory(...fnArgs)
  }
}
const dic = new DIC()
dic.register('frogOwner', Client)
dic.register('frogOwnerLicense', sallysLicense)
dic.register('frog', mikeTheToad)
dic.factory('frog-owner', FrogParadiseOwner)

const frogOwner = dic.get('frog-owner', [
  'frogOwner',
  'frogOwnerLicense',
  'frog',
])

console.log('frog-owner', JSON.stringify(frogOwner, null, 2))

در هر حال، نتیجه مشابهی به دست می‌آید:

{
  "id": "u_k8q16rjx_fgrw6b0yb528unp3trokb",
  "license": {
    "id": "m_k8q16rjk_jipoch164dsbpnwi23xin",
    "client": {
      "firstName": "sally",
      "lastName": "tran",
      "id": "b_k8q16rjk_0xfqodlst2wqh0pxcl91j"
    },
    "preparer": {
      "firstName": "richard",
      "lastName": "rodriguez",
      "id": "g_k8q16rjk_f13fbvga6j2bjfmriir63"
    },
    "frog": {
      "name": "mike",
      "gender": "male",
      "weight": 12.5
    },
    "location": "undefined undefined NY 92804"
  },
  "frog": {
    "name": "mike",
    "gender": "male",
    "weight": 12.5
  }
}

به این ترتیب به پایان این مقاله می‌رسیم. امیدواریم از این راهنما بهره لازم را برده باشید.

اگر این مطلب برای شما مفید بوده است، ‌آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۱ نفر
آیا این مطلب برای شما مفید بود؟
شما قبلا رای داده‌اید!
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
better-programming

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *