آشنایی با Zenject در یونیتی

زنجکت یک فریم ورک بهینه برای تزریق وابستگی در یونیتی است.https://github.com/modesttree/Zenjectقبل از اینکه درمورد نحوه کار با Zenject توضیح دهیم بهتر است بدانیم که چرا باید از Zenject استفاده کنیم؟مزایای استفاده از Zenjectوقتی از یک فریم ورک DI استفاده می کنید ، طبیعتاً design pattern ها را بهتر دنبال خواهید کرد ، زیرا این امر شما را مجبور می کند که بیشتر در مورد interface بین کلاس ها فکر کنید.وقتی از تزریق وابستگی استفاده می کنید و کد loosely coupled است؛ خیلی راحت تر می شود Refactor کرد. شما می توانید بخشی از code base را کاملاً تغییر دهید بدون اینکه این تغییرات باعث خرابی قسمت های دیگر شود.نوشتن Unit Test یا User-Driven Tests خیلی راحت تر می شود.تزریق وابستگی تزریق وابستگی ( Dependency Injection یا به اختصار DI) فرآیندی می باشد که هدف اصلی آن حذف وابستگی‌ های موجود بین دو کلاس با استفاده از یک رابط (Interface) است.تزریق وابستگی vs روش سنتیزمانی که یک کلاس مانند کلاس A به کلاس B وابستگی دارد و نیازمند یک شی از کلاس B برای انجام عملیات مورد نظر خود است، شاید اولین راهی که به نظر برسد تعریف یک فیلد از کلاس B در کلاس A است.با استفاده از این فیلد کلاس A می تواند عملیات مورد نظر خود را انجام دهد. متاسفانه، این روش باعث ایجاد Tight Coupling یا همان در هم تنیدگی کلاس ها می شود.روش بهتر انجام این وابستگی در قالبی است که باعث Loose Coupling یا همان وابستگی سست گردد.حذف وابستگی بین کلاس A و B با استفاده از تزریق وابستگیوارونگی کنترلوارونگی کنترل ( Inversion of Control یا به اختصار IOC ) ، روش بهبود یافته ی تزریق وابستگی است.در واقع، IOC Container به عنوان یک کلاس وظیفه انجام هر چه ساده تر Dependency Injection را به عهده دارد.آشنایی با مفاهیم و شیوه استفاده IOC Containerمفهوم ContractTypeنوعی که می خواهید برای آن یک اتصال (Binding) ایجاد کنید.مفهوم ResultTypeنوعی که به آن متصل شود.مفهوم Scopeاین مقدار تعیین می کند که نمونه تولید شده هر چند وقت یک بار (یا در صورت وجود) در چندین تزریق استفاده شود.انواع Scopeبه عنوان گذرا AsTransientهر بار که درخواست داده می شود یک نمونه ساخته می شود حتی در صورت وجود نمونه قبلی دوباره از آن استفاده نمی شود.به عنوان ذخیره شده AsCachedهر بار که درخواست داده می شود در صورت وجود نمونه قبلی دوباره از آن استفاده می شود.به عنوان تک AsSingleیک نمونه فقط ساخته می شود و در تمام طول عمر بازی استفاده می شود.به بیان دیگر این روش خیلی شبیه AsCached هست با این تفاوت که یک نمونه بیشتر از آن وجود ندارد.چرا از IOC Container استفاده کنیم؟در حالت عادی مجبور هستیم برای دسترسی به کلاس مورد نظر یک شئ از آن بسازیم.این روش جالبی نیست چرا که ممکن است بعداً بخواهیم یک AudioService دیگر جایگزین آن کنیم در نتیجه ناچاریم به صورت دستی تمام Audio Service ها را تغییر دهیم.پس بهتره یک نفر وظیفه ساختن نمونه ها رو بر عهده بگیره و اون کسی نیست جز IOC Container که فریم ورک Zenject آن را برای ما پیاده سازی کرده است.روش بهتر استفاده از Constructor و پاس دادن اینترفیس IAudioService است.برای اینکه از قابلیت IOC Container استفاده کنید باید کلاسی بسازید که از MonoInstaller استفاده کند.سپس می توانیم اینترفیس IAudioService را به کلاس AudioService متصل کنیم.به این معنی که هروقت به اینترفیس IAudioService دسترسی پیدا کردیم یک شئ از کلاس AudioService ساخته شود.مزیت استفاده از Zenject این است که خیلی راحت می توانیم به جای AudioService هر کلاس دیگری مثل کلاس DebugAudioService را جایگزین کنیم.در نهایت می توانیم کلاس EnemySpawner را Bind کنیم.در نتیجه بعد از ساختن شئ از این کلاس اینترفیس IAudioService به این کلاس پاس داده می شود.در اینجا از NonLazy استفاده شده است. در حالت عادی ResultType فقط زمانی وجود دارد که اتصال برای اولین بار استفاده شود. با این حال ، هنگامی که از NonLazy استفاده می شود ، ResultType بلافاصله هنگام راه اندازی ایجاد می شود.مثال عامیانه برای درک بهتر حالت Lazy و NonLazyدر حالت تنبل (Lazy) وقتی که بهش بگویید برو نان بخر می رود نان بخرد.ولی در حالت غیر تنبل (NonLazy) بدون اینکه بهش بگویند نان را می خرد.به بیان ساده تر حالت NonLazy همانند ساختن شئ در تابع Awake عمل می کند. تزریق وابستگی در GameObject تزریق وابستگی در کلاس هایی که از MonoBehaviour ارث می برند که در واقع یک GameObject هستند متفاوت است. چون در حالت عادی Constructor ندارند!سوال: چطور ما یک نمونه از IAudioService را به یک GameObject تزریق کنیم؟برای رفع این مشکل می توانیم از اتربیوت [Inject] استفاده کنیم.وقتی بازی اجرا می شود Zenject تمام Method هایی که از اتربیوت [Inject] استفاده می کنند را پیدا می کند و تلاش می کند وابستگی های آن را برطرف کند.در بعضی موارد بهتره از MonoBehaviour استفاده نکنید چون باعث میشه کدتون سنگین بشه.به جاش می تونید از Interface هایی که Zenject در اختیارتون گذاشته استفاده کنید. آشنایی با Signal Busبه بیان خیلی ساده Signal ها در Zenject شبیه Event عمل می کند و همچنین از دیزاین پترن Observer پیروی می کند.دیزاین پترن Observerبه شما این امکان رو میده که یک شئ به نام (subject) و فهرست وابستگی هاش به نام (observers) نگه دارید.بعد هر اتفاقی که روی شئ بیوفته به ناظران خبر بدید.بررسی مثالی از Signal Busروش پیاده سازی دیزاین پترن Observer بدون استفاده از Zenject به صورت زیر است:موقع Enable شدن سیگنال را اضافه و موقع Disable شدن سینگال را باید حذف می کنیم. حال بهتر است روش پیاده سازی Observer در Zenject را بررسی کنیم:موقع Initialize شدن سیگنال را اضافه یا Subscribe و موقع Dispose شدن سینگال را حذف یا Unsubscribe می کنیم.