آشنایی با تزریق وابستگی یا Dependency Injection

نویسنده : سید ایوب کوکبی ۲۷ آذر ۱۳۹۷

در این پست قصد داریم شما را با مفهوم Dependency Injection یا به اختصار DI که در فارسی «تزریق وابستگی» ترجمه شده، آشنا کنیم. سعی می‌کنیم به زبان ساده این مفهوم را شرح دهیم تا آشنایی اولیه‌ای نسبت به آن داشته باشید. مثال‌ها به زبان کاتلین هستند.

تزریق وابستگی

وابستگی یا Dependency چیست؟

به این مثال توجه کنید:

class ClassA {
  var classB: ClassB
}

class ClassB {
  var classC: ClassC
}

class ClassC {
}

همانطور که می‌بینید، داخل ClassA نمونه‌ای از ClassB وجود دارد بنابراین کلاس A به کلاس B وابسته است. چرا؟ چون کلاس A برای انجام دادن کارش به کلاس B نیاز دارد. به بیان دیگر کلاس B یک وابستگی (dependency) برای کلاس A محسوب می‌شود. وابستگی لزوماً چیز بدی نیست. اینکه یک کلاس همۀ کارها را به تنهایی انجام ندهد خوب است. ما ناچاریم منطق برنامه را بین چندین کلاس با مسئولیت‌های مختلف تقسیم کنیم تا در کنار یکدیگر وظایف محوله را انجام دهند.

روش‌های کنترل وابستگی

به سه روش می‌توان وابستگی‌ها را کنترل یا اصطلاحاً handle کرد.

روش اول – هندل کردن وابستگی‌ها داخل کلاس وابسته

در این روش هر جایی نیاز به آبجکتی از کلاس داشتیم همانجا ایجاد می‌کنیم. 

class ClassA {
  var classB: ClassB
  fun someMethodOrConstructor() {
    classB = ClassB()
    classB.doSomthing()
  }
}

داخل کلاس A که یک کلاس وابسته محسوب می‌شود، متغیری از نوع ClassB تعریف کرده‌ایم و داخل یکی از متدها یا سازنده‌ها، classB را نمونه‌سازی و متد ()doSomthing را فرخوانی کرده‌ایم. همان کاری که اغلب ما برنامه‌نویس‌ها انجام می‌دهیم!

مزایای این روش

  • ساده و سرراست است؛
  • کلاس وابسته (در مثال بالا classA) کنترل کاملی روی وابستگی‌ها و زمان ایجاد آن‌ها دارد.

معایب این روش

  • ClassA و ClassB بسیار درهم تنیده یا اصطلاحاً tightly coupled هستند. معنی‌اش این است که هر وقت به کلاس A نیاز باشد حتماً باید از کلاس B هم استفاده کنیم. بنابراین جایگزین کردن ClassB با یک کلاس دیگر عملاً غیرممکن است.
  • هر تغییری در شیوۀ نمونه‌سازی کلاس B نیازمند انعکاس تغییرات در کلاس A و سایر کلاس‌های وابسته به آن است. مثلاً اگر به سازندۀ کلاس B پارامتری اضافه کنیم، باید به تمام کلاس‌های وابسته برویم و نمونه‌سازی‌های کلاس B را بروزرسانی کنیم. در روش حاضر، تغییر وابستگی‌ها بسیار دشوار است؛
  • کلاس A غیرقابل تست است. در پست آموزش یونیت تست و TDD به صورت عملی، دانستیم که شرط تست‌پذیری یک کلاس، ایزوله بودن آن است. یعنی شما زمانی می‌توانید کلاسی را یونیت تست کنید که مستقل بوده و به کلاس‌های دیگر وابسته نباشد. در مثال بالا چنین شرایطی برقرار نیست. ممکن است آزمون واحد شما از کلاس A به خاطر یک متد از کلاس B با مشکل مواجه شود. در واقع اینجا کلاس A که Unit ماست برای تست شدن ایزوله نیست؛
  • یکی دیگر از مشکلات روش حاضر، نقض قاعدۀ تک مسئولیتی یا Single Responsibility است که یکی از مهم‌ترین اصول Solid به شما می‌رود.

هر کلاس، فقط و فقط باید مسئول انجام یک کار باشد

ما نمی‌خواهیم کلاسمان به جزء وظیفۀ اصلی‌اش نگران چیز دیگری باشد. برای خلاصی از شر این وابستگی‌ها باید چه کنیم. روش دوم تا حدودی ما را از این وابستگی نجات می‌دهد.

بخوانید  نکاتی برای موفقیت در برنامه‌نویسی اندروید

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

خب، تا الان متوجه شدیم که هندل کردن وابستگی‌ها داخل کلاسِ وابسته روش خوبی نیست. حال چطور می‌شود اگر کلاس وابسته همه وابستگی‌ها را مثلاً داخل یک سازنده تعریف کند و فراهم‌سازی آن را به یک user class بسپارد. ببینیم این کار مشکل را حل می‌کند یا نه:

class ClassA {
	var classB: ClassB
	constructor(classB: ClassB){
		this.classB = classB
	}
}

class ClassB {
	var classC: ClassC
	constructor(classC: ClassC){
		this.classC = classC
	}
}

class ClassC {
	constructor(){
	}
}

class UserClass(){
	fun doSomething(){
		val classC = ClassC();
		val classB = ClassB(classC);
		val classA = ClassA(classB);
		classA.someMethod();
	}
}

اکنون ClassA همۀ وابستگی‌ها را داخل سازنده‌اش دریافت می‌کند و بدون initial کردن می‌تواند متدهای کلاس B را صدا بزند.

مزایا

  • کلاس A و کلاس B اکنون loosely coupled هستند یا به عبارت وابستگی سستی با هم دارند. بنابراین بدون اینکه مشکلی برای کدهای قبلی بوجود بیاد می‌توانیم کلاس B را با کلاس دیگری داخل کلاس A تعویض کنیم. مثلاً به جای پاس کردن ClassB به سازندۀ کلاس A اکنون می‌توانیم AssumeClassB که یک زیرکلاس از کلاس B است را به آن ارسال کنیم. در این حالت کدها مثل قبل و بدون بروز مشکل، کار خود را انجام می‌دهند؛
  • اکنون کلاس A قابل تست است. در هنگام نوشتن یونیت تست، می‌توانیم ClassB را mock کنیم و به کلاس A بفرستیم. حالا هر خطایی رخ بدهد مربوط به کلاس A خواهد بود؛
  • کلاس A دیگر نگران وابستگی‌ به کلاس‌های دیگر نیست و می‌تواند روی وظیفۀ اصلی‌اش تمرکز کند.

معایب

  • اینجا یک زنجیر به وجود آمده است. مثلاً کاربرِ کلاس A باید همه چیز را دربارۀ initial کردن کلاس B بداند که کلاس B هم به نوبۀ خود باید دربارۀ کلاس C مطلع باشد و همینطور مثل حلقه‌های زنجیر الی آخر. بنابراین هر تغییری در سازندۀ یکی از این کلاس‌ها روی callerهای آن تاثیر خواده گذاشت؛
  • اگرچه وابستگی‌ها در روش فعلی راحت و قابل درک‌تر است ولی کدِ کاربر پیچیده و مدیریت آن سخت می‌شود. بنابراین مزیت سادگی کد را از دست خواهیم داد. همچنین اصل تک مسئولیتی هم نقض می‌شود چون کدهای کاربر علاوه بر وظایف خودش باید وابستگی‌های لازم برای کلاس‌های وابسته را هم هندل کند.

مثال دوم به مراتب بهتر از مثال اول است ولی هنوز هم مشکلاتی دارد. آیا می‌توانیم این چند مشکل باقی‌مانده را هم حل کنیم. بله با کمک تزریق وابستگی. ولی قبل از توضیح روش سوم ابتدا دربارۀ dependency injection صحبت کنیم.

تزریق وابستگی یا Dependency Injection چیست؟

تزریق وابستگی روشی برای هندل کردن وابستگی‌ها خارج از کلاس وابسته است طوری که دیگر کلاس وابسته نگران این موضوع نخواهد بود.

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

بخوانید  توصیه‌هایی به توسعه‌دهندگان اندروید

چون در تعریف ویکی‌پدیا از تزریق وابستگی، جایی که باید وابستگی‌ها هندل شوند را مشخص نکرده است. فقط گفته شده وابستگی‌ها را نباید داخل کلاس وابسته قرار داد. این به برنامه‌نویس بستگی دارد که مکان مناسبی برای هندل کردن وابستگی‌ها در نظر بگیرد و همانطور که می‌بینید در مثال دوم، user class جای مناسبی برای این کار نیست! آیا می‌توانیم مثال دوم را بهبود دهیم. بله؛ روش سوم را بخوانید.

روش سوم – هندل کردن وابستگی‌ها بر عهدۀ یک نفر دیگر گذاشته شود

در رویکرد اول، کلاس وابسته خودش مسئول وابستگی‌ها بود و در رویکرد دوم این مسئولیت را به user class دادیم. اما هر دو مورد نگران هندل کردن وابستگی‌ها هستند. اینجاست که تزریق وابستگی به کمک ما می‌آید.

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

فریم‌ورک تزریق وابستگی ممکن است قابلیت‌های زیادی داشته باشد ولی دو ویژگی آن از همه مهم‌تر است. اول اینکه اجازه می‌دهند تا فیلدها یا اشیاء تزریق را تعریف کنیم که برخی از فریم‌ورک‌ها این کار را با annotate کردند فیلدها یا قرار دادن inject@ در ابتدای سازندۀ کلاس انجام می‌دهند. برای مثال Koin از ویژگی‌های درونی کاتلین برای این کار استفاده می‌کند. با تزریق یا inject کردن، مدیریت وابستگی‌ها را بر عهدۀ فریم‌ورک تزریق وابستگی قرار می‌دهیم. مثلاً کد پایین را ببینید:

class ClassA {
  var classB: ClassB
  @Inject constructor(classB: ClassB){
    this.classB = classB
  }
}

class ClassB {
  var classC: ClassC
  @Inject constructor(classC: ClassC){
    this.classC = classC
  }
}

class ClassC {
  @Inject constructor(){
  }
}

ویژگی دوم این است که اجازه دهد در یک فایل کاملاً جدا وابستگی‌ها را تامین کند. چیزی شبیه مثال پایین. البته با توجه به نوع فریم‌ورکی که انتخاب می‌کنید ممکن است تغییراتی در این کد مشاهده کنید.

class OurThirdPartyGuy {
  fun provideClassC(){
    return ClassC() //just creating an instance of the object and return it.
  }

  fun provideClassB(classC: ClassC){
    return ClassB(classC)
  }

  fun provideClassA(classB: ClassB){
    return ClassA(classB)
  }
}

همانطور که می‌بینید هر تابعی فقط مسئول یک وابستگی است. بنابراین اگر جایی در برنامه به ClassA نیاز داشتید، فریم‌ورک تزریق وابستگی خودش با فرخوانی تابع ()provideClassC نمونه‌ای از کلاس C ساخته، آن را به ()provideClassB می‌فرستد و نمونه ساخته شده از کلاس B را به ()provideClassA فرستاده و از آنجا نمونۀ نهایی یعنی کلاس A را به مصرف‌کننده تحویل می‌دهد. همۀ این کارها را فریم‌ورک انجام می‌دهد بدون اینکه نیازی به مداخله از سوی ما باشد. حال ببینیم مزایا و معایب این روش چیست.

بخوانید  Clean Code - کتابی که هر توسعه‌دهنده‌ای باید بخواند

مزایا

  • درک سادۀ کدها، هم کلاس‌های وابسته و هم قسمتی که وابستگی‌ها را فراهم می‌کند ساده و قابل درک هستند؛
  • کلاس‌ها وابستگی ندارند بنابراین loosely coupled هستند و به راحتی می‌توان با یک کلاس دیگر جایگزین کرد. مثلاً فرض کنید می‌خواهیم ClassC را با AssumeClassC که یک زیرکلاس از آن است جایگزین کنیم. کافی است طبق کد زیر جایگزینی را انجام دهیم. از این پس هر جایی که کلاس C را فرخوانی کردیم به صورت خودکار با AssumeClassC جایگزین خواهد شد:
fun provideClassC(){
  return AssumeClassC()
}

دقت کنید ما برای این جایگزینی هیچ جایی از برنامه را تغییر ندادیم. این روش واقعاً ساده و انعطاف‌پذیر است.

  • کاملاً قابل تست است. به راحتی می‌توانیم در چارچوب تزریق وابستگی، وابستگی‌ها را با نسخۀ Mock یا جعلی آن‌ها جایگزین کنیم تا در حین تست با مشکلی بر نخوریم. در واقع تزریق وابستگی دوست درجۀ یک شما برای انجام آزمون‌های واحد است؛
  • طراحی برنامه خیلی ساده‌تر می‌شود. ما اکنون بخش مجزایی برای مدیریت وابستگی‌ها داریم. مابقی بخش‌ها به راحتی می‌توانند روی کار اصلی خود تمرکز کنند بدون اینکه نگران این وابستگی‌ها باشند.

معایب

  • فریم‌ورک‌های تزریق وابستگی کمی زمان برای یادگیری لازم دارند. بنابراین شما و اعضای تیمتان قبل از اینکه به صورت موثر از آن استفاده کنید باید زمانی را صرف یادگیری‌اش کنید.

نتیجه‌گیری

  • هندل کردن وابستگی‌ها بدون تزریق وابستگی ممکن است ولی مشکلات فراوانی به همراه دارد؛
  • تزریق وابستگی صرفاً یک ایدۀ پیاده‌سازی است و فقط حرفش این است که وابستگی‌ها را جایی خارج از کلاس‌های وابسته مدیریت کنیم؛
  • بهتر است بخش مجزایی از برنامه را به تزریق وابستگی اختصاص دهید. اغلب فریم‌ورک‌های تزریق وابستگی این کار را برای شما انجام می‌دهند؛
  • ضرورتی به استفاده از فریم‌ورک‌ها و کتابخانه‌های تزریق وابستگی نیست ولی قطعاً در ساده کردن کار شما نقش موثری خواهند داشت. بسیاری از این فریم‌ورک‌ها ویژگی‌های پیشرفته‌ای را با حداقل میزان کدنویسی فراهم می‌کنند که این موضوع سرعت پیشروی پروژه را بیشترمی‌کند.

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

سید ایوب کوکبی

نویسنده و مترجم...

0 دیدگاه

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