پوریا سیفی
پوریا سیفی
خواندن ۷ دقیقه·۵ سال پیش

سرویس کانتینر لاراول (IOC)

حتما تا به حال خیلی تلاش کردید مفهموم service container رو بفهمید. شاید اولین جایی که بهش مراجعه کردید و نا امید هم شدید قسمت IoC Container تو مستندات سایت لاراول باشه اما بعید میدونم درک و اطلاعات کاملی دستگیرتون شده باشه. از طرفی هم هیچوقت دلمون نمیخواد با نفمیدن موضوع به این مهمی کنار بیایم، چرا که درک مفهوم Laravel IoC container برای انجام پروژه هایی تو سطح متوسط و بزرگ ، فهم هسته و کانسپت (ایده) لاراول و یا مشارکت در ریپازیتوری لاراول بسیار حیاتی و ضروریه...

در واقع کسی که این مفهوم رو درک نکنه نمیتونه ادعای حرفه ای بودن تو فریم ورک لاراول رو داشته باشه. واقعیت امر اینه که اگر بخوایم این مفهوم رو به درستی متوجه بشیم باید یه مقدار از دورتر به ماجرا نگاه کنیم. پس با من همراه باشید که پرده از اسرار ioc بگشاییم :)

برای درک بهتر این مطلب لازمه با شی گرایی تا حدودی آشنا باشید و حداقل مفاهیمی مثل Class , Interface رو خوب بشناسید.

تو دنیای نرم افزار مفهومی به اسم inversion of control یا همون ioc وجود داره که صرقا تو فریم ورگ لاراول هم نیست. کار ioc مدیریت وابستگی های کلاس ها هست.

شاید واژه Dependency Injection (تزریق وابستگی) تو لاراول به گوشتون خورده باشه . اما این Dependency Injection واقعا چیه و اصلا چرا لازمه ؟ خب این اولین و مهم ترین سواله.

کد زیر رو در نظر بگیرید :

class FooBar { public function __construct(Baz $baz) { $this->baz = $baz; } }

فرض کنید قرار یه شی از این کلاس بسازیم . قاعدتا باید به این شکل عمل کنیم :

$fooBaz = new FooBar(new Baz);

شاید گفتنش براتون تکراری باشه ولی واضحه که اگه این وابستگی ها بیشتر از این حرفا باشه قطعا کدنویسی سخت و خسته کننده ای در طول پروژه به ما تحمیل میشه. خب بهتره اول یه راه تمیز تر با لاراول رو امتحان کنیم :

$fooBar = App::make('FooBar');

این شی دقیقا معادل شی اولی هست که ساختیم. اولین سوالی که اینجا پیش میاد اینه که تکلیف کلاس Baz چی میشه که تو متد سازنده کلاس FooBar اجباریه؟

سوال خوبیه :)

وقتی از مدیریت وابستگی توسط لاراول صحبت میکنیم دقیقا به همین اتفاق اشاره داریم. خب اینکه چطور این اتفاق افتاد برمیگرده به کدهای ریپازیتوری Container تو سورس کد لاراول. فعلا در همین حد بدونید که لاراول از یه ویژگی تو خود PHP استفاده میکنه به نام Reflection Class که به واسطه ی این کلاس میشه فهمید داخل یه کلاس دیگه چه خبره. مثلا میشه فهمید سازنده ی کلاس ما چه وابستگی هایی داره. بنابراین باراول ازین قابلیت PHP استفاده میکنه و تکلیف همه اون وابستگی ها رو مشخص میکنه و اینطوری میشه که تو کد بالا بدون اینکه ما به کلاس Baz اشاره ای بکنیم میتونیم از کلاس FooBar شی بسازیم.

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

خب در پاسخ به این سوال ها باید بگم که قضیه جدی تر از این حرفاست. بریم سراغ یه مثال کوچیک:

class MySQLConnection { public function connect() { var_dump(‘MYSQL Connection’); } } class PasswordReminder { private $dbConnection; public function __construct(MySQLConnection $dbConnection) { $this->dbConnection = $dbConnection; } }

تو این مثال ساده PasswordReminder همونطور که از اسمش پیداست قراره فراموشی رمز رو برای ما هندل کنه. همونطور که مشخصه سازنده این کلاس به MySQLConnection وابسته ست. حالا فرض کنید مجبور شیم به هر دلیلی دیتابیس رو از mysql به mongo تغییر بدیم (میدونم مثال خیلی عملی نیست فقط به عنوان یه چیزی که میشه لمسش کرد بهش فکر کنید). خب چه اتفاقی میفته؟

آیا این تغییر در تمام جاهایی از پروژه که از mysql استفاده کردیم باید اعمال شه؟

اگر با اصول SOLID آشنا باشید. اصل آخر ینی همون Dependency Inversion Principle یا اصل وارونه کردن وابستگی (DIP) تاکید میکنه که :

الف: ماژول های سطح بالا نباید به ماژول های سطح پایین وابستگی داشته باشند. هر دو این ماژول ها باید به یک سطح انتزاعی (Abstraction) وابسته باشند.

ب: Abstraction‌ها نباید به جزئیات وابسته باشند. جزئیات باید به Abstraction‌ها وابسته باشند.

نمیخوام خیلی درگیر تعاریف تکراری و خسته کننده شیم. پس بیاید به یه شکل دیگه کدهای مثالمون رو تغییر بدیم !

interface ConnectionInterface { public function connect(); } class MySQLConnection implements ConnectionInterface { public function connect() { var_dump(‘MYSQL Connection’); } } class PasswordReminder { private $dbConnection; public function __construct(ConnectionInterface $dbConnection) { $this->dbConnection = $dbConnection; } }

تو کد بالا اگر قرار باشه کانکشن رو از mysql به mongo تغییر بدیم هیچ خللی در روند برنامه پیش نمیاد. در واقع یکی از مزایای استفاده از اینترفیس ها همینه. اینجا ما سعی کردیم جزئیات ماژول سطح بالا ینی همون PasswordReminder رو به Abstraction‌ (منظور همون اینترفیس هست) وابسته کنیم نه ماژول سطح پایین (MySQLConnection).

خب حالا که این موضوع برامون روشن شد بیاید این مثال رو تو بستر لاراول هم امتحان بکنیم .

فرض کنیم تو کلاس PasswordReminder یه متد به اسم doSomething داریم که قراره از طریق روت زیر فراخوانی شه :

Route::post('remindPass', 'PasswordReminder@doSomething');

به نظرتون IOC لاراول باز هم قادره وابستگی ما رو هندل کنه ؟؟

پاسخ سوال منفیه و در صورت امتحان با ارور BindingResolutionException مواجه خواهیم شد.

دلیل هم مشخصه وقتی کلاس PasswordReminder داره ساخته میشه جایی اشاره نکردیم که وابستگی ش چه کلاسی هست.( فقط به اینترفیس اشاره کردیم)

خب راه حل چیه ؟!

اینجاست که کم کم اهمیت سرویس کانتینر رو درک میکنیم. با فراخوانی کد زیر در متد register کلاس AppServiceProvider مشکل رفع میشه

App::bind('ConnectionInterface ', 'MySQLConnection ');

در واقع با یک خط کد و فقط یکبار برای همیشه (bind کردن توسط سرویس کانتینر) به لاراول فهموندیم که کانکشن مد نظر ما mysql هست. حتی میتونیم واژه 'MySQLConnection' رو توی فایل های کانفیگ قرار بدیم. دقت کرده باشید این روند تو خیلی از قسمت های لاراول یا پکیج های دیگه هم اتفاق میفته. در واقع شما یه سری درایور دارید که فقط یک جا مشخص میکنید درایور مورد نظرتون چیه. مثلا درایور سشن در لاراول که میتونه فایل باشه، دیتابیس باشه یا ردیس باشه.

حالا ما برای راحتی و انسجام کانفیگ ها، گاها میایم تو یه فایل مشخص میکنیم درایور ها رو. ولی پشت پرده کد های لاراول از اون ها برای bind کردن استفاده میکنه.

یکی دیگه از قابلیت های مهم ioc تو لاراول استفاده از دیزاین پترن singleton هست. مثلا فرض کنید اتصال به دیتابیس که زمان نسبتا بیشتری میخواد فقط یکبار در طی درخواست کاربر باید انجام بشه. یا به هر دلیلی تصمیم داریم کلاس یکبار ساخته شه و تا انتهای درخواست ازش استفاده کنیم. استفاده از متد singleton این کار رو برای ما ممکن میکنه.

حتی با قابلیت هایی مثل contextual-binding میتونیم اشاره کنیم که برای کدوم کلاس ها کانکشن mysql و برای کدوم کلاس ها کانکشن mongo استفاده بشه.

البته متد های دیگه ای برای سرویس کانتینر وجود داره که میتونید تو داکیومنت سایت اصلی بررسی کنید.


مفهوم IOC عمدتا در طراحی فریم ورک ها مورد استفاده قرار میگیره و اگه دقت کنید خیلی از قسمت های لاراول دیده میشه و کاملا مشخصه که چقدر رو فرآیند توسعه تاثیر بسزایی میذاره. controllers و event listeners و middleware و... ازین ویژگی بهره میگیرن. فسادها و helper function ها هم که کدنویسی در لاراول رو برای ما خیلی راحت و شیرین کردند به همین صورت. دلیل این همه کاربرد هم یه جورایی برمیگرده به همون مدیریت صحیح و عالی وابستگی ها.

امیدوارم با خوندن این مقاله درک بهتری از ioc کسب کرده باشید. سعی میکنم مقاله رو به روز نگه دارم و هر چند وقت یکبار کامل تر کنم. این اولین مقاله نوشتاری من بود و امیدوارم کم و کاستی هاش رو ببخشید. اگر ایراد و اصلاحی هم وارده خوشحال میشم باهام در میون بگذارید.

تندرست و پیروز باشید


لاراولسرویس کانتینر
شاید از این پست‌ها خوشتان بیاید