درک عمیق‌تر Service Container و Service Provider در لاراول

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

Service Container
Dependency Injection Container (DI)
Inversion of Control Container (IoC)
Application Container

در واقع Service Container لاراول یکی از مهم‌ترین بخش‌های این فریمورکه که کمتر بهش توجه میشه، ۲ دلیل مهم برای این کم توجهی Developerها اینه:

۱- بعضی‌ها فکر می‌کنند که درک DI سخته، پس بهتره که بیخیال مفهوم IoC و IoC container بشیم.
۲- درکی از این ندارن که آیا قراره از container استفاده کنند یا خیر (در واقع ما همیشه داریم از این‌ها استفاده می‌کنیم؛ چرا که سنگ بنای لاراول روی Service Container بنا شده).

تزریق وابستگی (Dependency Injection) و IoC:

یک تعریف خیلی ساده از DI اینه: فرآیند پاس دادن وابستگی‌های یک کلاس به شکل آرگومان(هایی) به یکی از متدهای اون کلاس (معمولا setter یا constructor).
کد زیر، نمونه‌ای از کدی هست که توش DI رعایت نشده و فراخوانی کلاس پایه داخل کلاس سطح بالاتر اتفاق افتاده:

متد share مربوط به کلاس TwitterService می‌تونه یک چنین چیزی باشه:

توی کد بالا، داخل سازندهٔ کلاس Publication یک شی جدید از کلاس TwitterService ایجاد شده و به عبارتی کلاس Publication حالا به کلاس توییتر سرویس وابسته شده، به جای اینکه این کلاس رو داخل بدنهٔ سازنده فراخوانی کنیم؛ می‌تونیم شی‌ای از این کلاس رو به صورت یک آرگومان به سازندهٔ Publication پاس بدیم:

برای درک بهتر قطعه کدهای بالا، داخل web route میاییم و می‌گیم که هر وقت وارد uri روت یا / شدیم؛ یک توییت ارسال کن:

این ساده‌ترین حالتی بود که میشد DI رو پیاده‌سازی کرد؛ اعمال کردن DI به یک کلاس باعث IoC یا وارونگی کنترل میشه، یعنی کلاس سطح بالاتر (اینجا Publication) دیگه وابسته به کلاس سطح پایین‌تر (اینجا TwitterService) نیست. قبل از اعمال DI، کلاس وابسته (Publication) در کنترل یک وابستگی سطح پایین‌تر بود. اما با اعمال DI، کنترل این پروسه به فریمورک واگذار شد.


IoC Container

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

با استفاده از IoC Container میشه پروسهٔ DI رو خیلی ساده تر کرد. قطعه کد زیر یک کلاسه که می‌تونه دیتایی که می‌خوایم رو ذخیره و در وقت نیاز بازیابی کنه (به ما برگردونه). یک نمونهٔ خیلی ساده از IoC Container می‌تونه به شکل زیر پیاده‌سازی بشه:

اسم این کلاس رو گذاشتیم Container، چرا؟ چون مثل یک کانتینر یا ظرفی هست که میشه توش وابستگی‌ها رو تعریف کرد.

ما می‌تونیم هر دیتایی که دلمون خواست رو توی این کانتینر bind کنیم؛ متد bind دو تا آرگومان می‌گیره، کلید و مقدار، کلید و مقدار توی یک آرایه به اسم bindings ذخیره میشن و وقتی قرار باشه که ازشون استفاده کنیم کافیه کلید رو به متد make پاس بدیم تا اگر مقدار متناظرش یک تابع بود؛ اون رو رن کنه و اگر غیر اون بود، مقدار درخواستی رو برگردونه.

برای درک بهتر کلاس بالا، ازش توی web route استفاده می‌کنیم:

توی مثال بالا، اول یک شی از کلاس Container یا در واقع همون IoC Container خودمون ساختیم؛ بعدش با استفاده از متد bind یک key value رو جفت کردیم (در واقع داخل آرایهٔ bindings ذخیره کردیم)؛ حالا که به اون مقدار نیاز پیدا کردیم؛ کافیه key رو به متد make این کلاس پاس بدیم.

حالا خیلی راحت می‌تونیم با استفاده از یک callback function یک کلاس رو به IoC Container بایند کنیم؛ این callback function میاد و یک شی از کلاسی که بهش دادیم ایجاد می‌کنه و مسیر کامل کلاس (namespace) رو هم به عنوان کلید داخل bindings ذخیره می‌کنه.

مثلا توی قطعه کد بالا، کلید ما TwitterService::class هست که معادلش میشه App\Service\TwitterService (به شکل رشته).

حالا بیایین فرض کنیم که کلاس TwitterService نیاز به یک کلید API داره تا بتونه اهراز هویت رو انجام بده و بعدش توییت رو ارسال کنه؛ توی این حالت می‌تونیم کلاسمون رو به شکل زیر پیاده‌سازی کنیم:

چند نکتهٔ مهم در مورد قطعه کد بالا:

۱- یک شی از کلاس Container ایجاد کردیم.
۲- کلید API رو با متد bind به container متصل کردیم.
۳- اینجا مهمه: کلاس TwitterService رو به عنوان کلید در نظر گرفتیم و آرگومان دوم یک closure function هست که توش از use استفاده شده! خب این دیگه چه جور syntaxای هست؟

یک closure در واقعی تابعی هست که می‌تونیم اون رو به یک متغیر نسبت بدیم.
یک closure یک namespace جدا محسوب میشه، بنابراین به طور معمول امکان دسترسی به متغیرهایی که بیرون این عبارت تعریف شدن وجود نداره و باید از کلمهٔ کلیدی use برای دسترسی به اون مقادیر خارج از فضای نام استفاده کرد.

وقتی که ما یک دیتا رو به container بایند می‌کنیم؛ هر زمان که بهش نیاز داشته باشیم می‌تونیم صداش بزنیم بدون اینکه نیاز باشه تا همیشه از new استفاده کنیم؛ در واقع ما با استفاده از IoC Container فقط یک بار از new استفاده می‌کنیم.

به خودی خود استفاد چند باره از new بد نیست؛ اما این رو باید در نظر گرفت با هر بار صدا زدن new ما باید وابستگی‌های اون کلاسی که داریم ازش یک نمونه (instant) ایجاد می‌کنیم رو بهش بدیم که نیاز به دقت داره و به مرور زمان حوصله‌سر بر و تکراری میشه. اما با IoC Container دیگه نیاز نیست نگران باشیم؛ چرا که container مراقبت تزریق وابستگی های ما خواهد بود.

یک IoC Container کد ما رو خیلی زیاد منعطف می‌کنه. وضعیتی رو فرض کنید که قرار باشه کلاس TwitterService رو با یک کلاس دیگه (برای پست کردن مطلب توی یک سایت دیگه به غیر از توییتر) عوض کنیم؛ مثلا LinkedInService؛ حالا تکلیف چیه؟ کدهایی که بالا نوشتیم اصلا برای یک چنین سناریویی مناسب نیستن؛ برای جایگزین کردن کلاس TwitterService باید یک کلاس جدید ایجاد و به container متصلش کنیم؛ و تمامی مرجع‌ها به کلاس قبلی رو جایگزین کنیم.

اما کافیه که بریم سراغ interface ها توی php تا این مشکل رو حل کنیم و به راحتی سرویس‌های مختلف یا درایورهای مختلف رو عوض کنیم؛ به جای اینکه مجبور به بازنویسی مجدد کدها باشیم. برای حل معضل بالا، کافیه یک interface به اسم SocialMediaServiceInterface ایجاد کنیم:

حالا می‌تونیم کلاس TwitterService خودمون که اینترفیس SocialMediaServiceInterface رو implement می‌کنه پیاده‌سازی کنیم:

***نکته: داخل interface نمیشه property تعریف کرد و فقط مجاز به تعریف تابع هستیم.

یک اصلاح مهم: concrete class یعنی کلاسی که یک interface رو implement می‌کنه. این کلاس باید تمامی متدهای تعریف شده داخل اینترفیس رو پیاده‌سازی کنه.

خب حالا ما باید به جای کلاسی که interface رو implement کرده خود interface رو به container بایند کنیم. توی callback یک شی از کلاس TwitterService رو به شکل قبلی برمی‌گردونیم.

کد بالا دقیقا مثل کد قبلی که بدون اینترفیس بود؛ کار می‌کنه. ماجرا از جایی جالب میشه که ما قراره از LinkedIn به جای توییتر استفاده کنیم. به لطف وجود interface این کار رو میشه تو ۲ مرحلهٔ ساده انجام داد:

۱- پیاده‌سازی کلاس LinkedInService که اینترفیس رو implement می‌کنه:

۲- آپدیت کردن فراخوانی TwitterService با LinkedInService توی آرگومان دوم bind:

حالا ما یک شی از کلاس LinkedInService داریم که به container بایند شده. زیبایی این روش اینکه که تمامی کدهای قبلی ما سر جای خودشون هستن و با یک تغییر کوچیک درایور شبکه اجتماعی رو عوض کردیم. تا زمانی که یک کلاس اینترفیس SocialMediaServiceInterface رو implement کنه، می‌تونه به عنوان یک سرویس معتبر شبکهٔ اجتماعی به کانتینر bind بشه.

مبحث Service Container و Service Provider:

لاراول با یک IoC Container خیلی قوی‌تری نسبت به پیاده‌سازی سادهٔ ما (کلاس Container) ارائه میشه. اسم IoC Container لاراول Service Container هست؛ پس عملا Service Container یا موارد زیر فرقی با هم ندارند و فقط یک اسم هستن:

DI Container
IoC Container
Application Container
Service Container

اگر بخوایم از IoC Container لاراول به جای IoCC خودمون (کلاس Container) استفاده کنیم؛ باید به شکل زیر عمل کنیم؛ شکل بازنویسی شده کدهای ما اینطوریه:

توی هر برنامهٔ لاراولی app در واقع نمونه‌ای از container ما هست. در واقع این helper function میاد و یک شی یا نمونه از Service Container (اسم لاراول برای IoC Container خودش) برمی‌گردونه.

دقیقا مثل container سفارشی و ساده که خودمون توسعه دادیم؛ Service Container لاراول یک متد bind و make داره که برای اتصال و دسترسی به سرویس‌ها کاربرد داره.

این کانتینر لاراول (Service Container) یک متد دیگه هم داره به اسم singleton که در واقع اشاره داره به design pattern معروف سینگلتون؛ وقتی یک کلاسی رو به عنوان singleton به کانتینر بایند می‌کنیم؛ در این صورت تنها و تنها یک شی از اون کلاس در هر درخواست ایجاد میشه.

برای درک تفاوت bind و singleton bind بهتره که نگاهی به کدهای زیر بندازیم:

توی قطعه کد بالا، به Service Container لاراول (app) گفتیم که ۲ تا شی برای ما make کنه؛ دقت داشته باشید که این کلاس به صورت ساده به container بایند شده؛ مقادیر برگشتی ۲۶۲ و ۲۶۹ نشون میده که instanceهای متفاوتی ایجاد شده، اما اگر کلاس رو به شکل singleton بایند کنیم؛ نتیجه متفاوت خواهد بود:

حالا برای هر دو تا instance که گفتیم make بشه؛ فقط یک عدد (۲۶۲) برگشت داده شده و این نشون میده که بایند کردن کلاس به شکل singleton باعث میشه که فقط و فقط یک شی از اون کلاس ایجاد بشه.


مبحث Service Provider:

حالا که با Service Container لاراول، تابع کمکی app و متدهای bind singleton و make آشنا شدیم؛ حالا وقتشه که یاد بگیریم؛ این متدها رو کجا فراخوانی کنیم؟! مطمئنا نمی‌تونیم از کنترلرها و مدل‌ها برای این کار (فراخوانی این متدها) استفاده کنیم.

محل درست برای قرار دادن binding ها Service Provider هست؛ Service Providerها در واقع کلاس‌هایی هستند که داخل پوشهٔ app/Providers قرار گرفتن و ما می‌تونیم SPهای سفارشی خودمون رو هم ایجاد کنیم. SP ها در واقع زیربنا و چارچوب فریمورک لاراول هستن؛ این کلاس ها مسئول راه‌اندازی اکثر سرویس‌هایی هستن که فریمورک ارائه میده.

هر پروژهٔ جدید با ۵ Service Provider به شکل default ارائه میشه. بین این‌ها، کلاس AppServiceProvider به شکل خالی ارائه به همراه ۲ متد boot و register ارائه میشه. متد register برای ثبت سرویس‌های جدید برای application استفاده میشه؛ این متد جایی هست که ما عملیات binding رو انجام می‌دیم:

داخل Service Providerها ما با استفاده از this به app دسترسی داریم و نیازی نیست که که تابع کمکی app استفاده کنیم؛ چرا که هر Provider وارث ServiceProvider هست پس میشه بهش دسترسی داشت.
متد boot برای منطق مورد نیاز برای راه‌اندازی سرویس‌های ثبت شده به کار میره. یک مثال خوب برای درک این قضیه کلاس BroadcastingServiceProvider هست که به شکل دیفالت با هر پروژهٔ لاراولی نصب میشه:

همون‌طور که می‌بینید؛ متد boot میاد و متد routes از فساد Broadcast رو صدا می‌زنه و همین‌طور فایل routes/channels.php رو require می‌کنه؛ با این کار مسیرهای broadcasting رو توی این پروسه فعال می‌کنه.

برای یک یا دو binding ساده، مثل چیزی که خودمون پیاده‌سازی کردیم؛ استفاده از کلاس AppServiceProvider منطقی به نظر می‌رسه؛ اما برای سرویس‌هایی که نیاز به منطق پیچیده‌تری برای اجرا شدن دارن، ما باید یک Provider جدید ایجاد کنیم:

artisan make:provider <provider name>


تصویر کامل!

توی بخش‌های قبلی مفاهیم مختلف زیر رو یاد گرفتیم:

۱- تزریق وابستگی (DI)
۲- اصل وارونگی کنترل (IoC)
۳- Service Container
۴- Service Providers

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

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

حالا کلاس Publication عوض اینکه وابسته به یک کلاس خاص باشه؛ می‌تونه هر نوع کلاسی که اینترفیس SocialMediaServiceInterface رو implement کرده؛ به عنوان آگومان ورودی دریافت کنه. قبل از این ما اینترفیس SocialMediaServiceInterface رو داخل Service Provider به کلاس LinkedInService که SocialMediaServiceInterface رو implement کرده بود bind کردیم؛ بنابراین با اجرای کد زیر؛ انتظار میره که یک شی از کلاس LinkedInService برگشت داده بشه:

app()->make(SocialMediaServiceInterface::class);

اما یک کلاسی که هنوز به Service Container لاراول bind نشده؛ خود کلاس Publication هست؛ اما اگر بیاییم و بدون bind کردن مستقیم کد زیر رو ران کنیم؛ چه اتفاقی می‌افته؟

app()->make(Publication::class);

می‌بینیم که به شکل عجیبی بدون bind کردن این کلاس؛ یک شی ازش ایجاد شد! توی نگاه اول این اتفاق عجیب و شبیه جادو هست؛ اما در واقع این اتفاق نتیجهٔ در کنار هم قرار گرفتن تمامی مفاهیمی هست که پیش از این گفتیم.

زمانی که لاراول به خط زیر می‌رسه:

app()->make(Publication::class);

میره دنبال مقدار متناظر با این کلید داخل Service Container؛ اما وقتی کلید رو پیدا نمی‌کنه؛ میره و یه نگاه می‌ندازه به constructor تا ببینه که این کلاس چه ورودی(هایی) می‌گیره:

لاراول متوجه میشه که کلاس Publication یک ورودی می‌گیره به اسم $socialMediaService که نوعش (که با type hint مشخص شده) SocialMediaServiceInterface اینه؛ بنابراین میره داخل Service Container و دنبال کلیدی می‌گرده که از این interface باشه؛ حالا لاراول وقتی که کلید
SocialMediaServiceInterface::class رو پیدا کرد؛ یک شی از نوع LinkedInService برمی‌گردونه:

الان باید برای شما مشخص شده باشه که لاراول می‌تونه به صورت اتوماتیک یک شی از وابستگی‌هایی که مجهز به Type Hint هستن (قبل از اسم متغیر نوع اون هم مشخص شده)؛ ایجاد کنه (تا زمانی که اون وابستگی ها هیچ interfaceای رو implement نکرده باشن).

حالا می تونیم routes/web.php رو به شکل زیر آپدیت کنیم:

حالا پروژه داره خیلی راحت کار می‌کنه؛ همون‌طور که می‌بینید؛ مطابق قابلیت تفکیک اتوماتیک container شما خیلی به ندرت به شکل دستی یک شی از container بیرون می‌کشید! تا زمانی که شما interfaceها رو به شکل مناسب bind می‌کنید و برای نیازمندها و وابستگی ها type hint قرار می دید؛ لاراول کارهای دشوار رو برای شما به صورت اتوماتیک انجام میده.


منبع: farhan.dev
(مطلب اصلی از این سایته؛ اما گاهی نکاتی از stackoverflow و... هم اضافه شده)