زنجکت یک فریم ورک بهینه برای تزریق وابستگی در یونیتی است.
https://github.com/modesttree/Zenject
قبل از اینکه درمورد نحوه کار با Zenject توضیح دهیم بهتر است بدانیم که چرا باید از Zenject استفاده کنیم؟
وقتی از یک فریم ورک DI استفاده می کنید ، طبیعتاً design pattern ها را بهتر دنبال خواهید کرد ، زیرا این امر شما را مجبور می کند که بیشتر در مورد interface بین کلاس ها فکر کنید.
وقتی از تزریق وابستگی استفاده می کنید و کد loosely coupled است؛ خیلی راحت تر می شود Refactor کرد. شما می توانید بخشی از code base را کاملاً تغییر دهید بدون اینکه این تغییرات باعث خرابی قسمت های دیگر شود.
نوشتن Unit Test یا User-Driven Tests خیلی راحت تر می شود.
تزریق وابستگی ( Dependency Injection یا به اختصار DI) فرآیندی می باشد که هدف اصلی آن حذف وابستگی های موجود بین دو کلاس با استفاده از یک رابط (Interface) است.
زمانی که یک کلاس مانند کلاس A به کلاس B وابستگی دارد و نیازمند یک شی از کلاس B برای انجام عملیات مورد نظر خود است، شاید اولین راهی که به نظر برسد تعریف یک فیلد از کلاس B در کلاس A است.
با استفاده از این فیلد کلاس A می تواند عملیات مورد نظر خود را انجام دهد. متاسفانه، این روش باعث ایجاد Tight Coupling یا همان در هم تنیدگی کلاس ها می شود.روش بهتر انجام این وابستگی در قالبی است که باعث Loose Coupling یا همان وابستگی سست گردد.
وارونگی کنترل ( Inversion of Control یا به اختصار IOC ) ، روش بهبود یافته ی تزریق وابستگی است.
در واقع، IOC Container به عنوان یک کلاس وظیفه انجام هر چه ساده تر Dependency Injection را به عهده دارد.
مفهوم ContractType
نوعی که می خواهید برای آن یک اتصال (Binding) ایجاد کنید.
مفهوم ResultType
نوعی که به آن متصل شود.
مفهوم Scope
این مقدار تعیین می کند که نمونه تولید شده هر چند وقت یک بار (یا در صورت وجود) در چندین تزریق استفاده شود.
به عنوان گذرا AsTransient
هر بار که درخواست داده می شود یک نمونه ساخته می شود حتی در صورت وجود نمونه قبلی دوباره از آن استفاده نمی شود.
به عنوان ذخیره شده AsCached
هر بار که درخواست داده می شود در صورت وجود نمونه قبلی دوباره از آن استفاده می شود.
به عنوان تک AsSingle
یک نمونه فقط ساخته می شود و در تمام طول عمر بازی استفاده می شود.
به بیان دیگر این روش خیلی شبیه AsCached هست با این تفاوت که یک نمونه بیشتر از آن وجود ندارد.
در حالت عادی مجبور هستیم برای دسترسی به کلاس مورد نظر یک شئ از آن بسازیم.
این روش جالبی نیست چرا که ممکن است بعداً بخواهیم یک 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 عمل می کند.
تزریق وابستگی در کلاس هایی که از MonoBehaviour ارث می برند که در واقع یک GameObject هستند متفاوت است. چون در حالت عادی Constructor ندارند!
سوال: چطور ما یک نمونه از IAudioService را به یک GameObject تزریق کنیم؟
برای رفع این مشکل می توانیم از اتربیوت [Inject] استفاده کنیم.
وقتی بازی اجرا می شود Zenject تمام Method هایی که از اتربیوت [Inject] استفاده می کنند را پیدا می کند و تلاش می کند وابستگی های آن را برطرف کند.
در بعضی موارد بهتره از MonoBehaviour استفاده نکنید چون باعث میشه کدتون سنگین بشه.
به جاش می تونید از Interface هایی که Zenject در اختیارتون گذاشته استفاده کنید.
به بیان خیلی ساده Signal ها در Zenject شبیه Event عمل می کند و همچنین از دیزاین پترن Observer پیروی می کند.
به شما این امکان رو میده که یک شئ به نام (subject) و فهرست وابستگی هاش به نام (observers) نگه دارید.بعد هر اتفاقی که روی شئ بیوفته به ناظران خبر بدید.
بررسی مثالی از Signal Bus
روش پیاده سازی دیزاین پترن Observer بدون استفاده از Zenject به صورت زیر است:
حال بهتر است روش پیاده سازی Observer در Zenject را بررسی کنیم: