به نام خدا
در این نوشته می خواهم در مورد چند مفهوم در طراحی نرم افزار توضیح بدهم:
1- DIP (Dependency Inversion Principle)
2- IOC (Inversion Of Control)
3- DI (Dependency Injection)
4- IOC Container
قبل از شروع باید با دو مفهوم اصول طراحی و الگو طراحی آشنا بشویم و موارد بالا را در این دو دسته بندی، جای بدهیم:
اصول طراحی (Design Principle):
اصول طراحی، اصول و نقشه راه سطح بالایی می باشد که برای طراحی بهتر نرم افزار پیشنهاد میشود و راه کاری برای پیاده سازی پیشنهاد نمی شود، بلکه مدل هایی مفهومی و قوانینی را برای طراحی بهتر پیشنهاد می کند.
مثال: SOLID - KISS - DRY - IOC
الگو های طراحی (Design Pattern):
الگوهای طراحی راه کار های سطح پایین می باشند که به طور مشخص راه کار و نحوه پیاده سازی با استفاده از زبان های برنامه نویسی مختلف را ارائه می کند و به زبان ساده راه حل های عملی و قابل پیاده سازی برای طراحی بهتر می باشد که بر پایه اصول های طراحی (Design Principles) پیاده سازی شده اند.
مثال: Factory Method - Singleton
حال می توانیم موراد بالا را مطابق تصویر زیر دسته بندی کنیم:
این موضوع را با تعریف رابرت مارتین (Robert Martin) شروع میکنیم:
High-level modules should not depend on low-level modules. Both should depend on abstractions and Abstractions should not depend upon details. Details should depend upon abstractions
ماژول ها یا کلاس های بالا دستی نباید به ماژول ها یا کلاس های پایین دستی وابسته باشند بلکه هر دو باید به Abstraction وابسته باشند و همچنین جزئیات باید به Abstraction وابسته باشند نه Abstraction به جزئیات.
برای درک این اصل به مثال های زیر توجه کنید:
مثال اول: شارژر و موبایل، شما فرض کنید یک موبایل را با شارژر به پریز برق وصل می کنید، موبایل از شارژر جهت وصل شدن و شارژ باتری استفاده می کند،درست است؟؟ حال اگر از شما بپرسند که چه کسی نوع شارژر (منظور نوع سوکت و مقدار ولتاژ و آمپر شارژر ) را تعیین می کند، پاسخ شما چیست؟ قطعا پاسخ شما موبایل می باشد چونکه نوع و برند گوشی مشخص کننده این موضوع می باشد پس به این نتیجه می رسیم که نوع شارژر، موبایل را مشخص نمی کند بله موبایل نوع شارژر مربوطه به خود را مشخص می کند. بنابراین ماژول های بالادستی، رابطی(Interface) را مشخص و یا تعریف می کنند و ماژول های پایین دستی آن رابط ها را پیاده سازی و استفاده می کنند.
مثال دوم: فرض کنید شما به عنوان یک برنامه نویس می خواهید یک برنامه کپی بنویسید مانند تصویر زیر که این برنامه از دو بخش خواندن (Read) و نوشتن (Write) تشکیل شده است.
همانطور که در عکس بالا می بینید برنامه کپی از دو بخش خواندن از کیبورد و نوشتن در فایل Text تشکیل شده است و با استفاده از یک زبان برنامه نویسی آن را پیاده سازی می کنیم، حال اگر بعد از چند وقت نیاز شد که به جای فایل در Text، در دیتابیس ذخیره شود مانند عکس پایین، چه می شود؟
با اضافه کردن نوشتن در دیتابیس، برنامه ما نیازمند تغییرات زیادی می باشد و ماژول بالا دستی که کپی می باشد هم باید تغییر کند چونکه به ماژول پایین دستی که نوشتن در دیتابیس می باشد وابسته می باشد، پس به این نتیجه می رسیم که ماژول کپی ما اصل DIP را رعایت نکرده است، برای اینکه همین ماژول را اصلاح کنیم مانند عکس زیر عمل می کنیم:
همانطور که در عکس بالا مشاهده می کنید ما با اضافه کردن رابط هایی (Interface) بین ماژول بالا دستی و ماژول پایین دستی این اصل را پیاده سازی و رعایت کردیم که با رعایت این اصل ما چه در بخش خواندن از (کیبورد، اسکنر و... ) و نوشتن در(فایل تکیت، پرینتر، دیتابیس و...) را اضافه کنیم بدون این که تغییری در ماژول بالادستی بخواهیم انجام دهیم، انجام خواهد شد.
در قسمت بالا DIP به ما توضیحی نمی دهد که مشکل عدم رعایت این اصل را چگونه پیاده سازی کنیم اما وارونگی کنترل (IOC) و یا دیگر معنی آن (واگذاری مسئولیت)، راه های رسیدن به DIP را به ما می گوید، تعاریف زیادی برای این موضوع وجود دارد اما میتوان گفت که این ساده ترین تعریف می باشد.
اگر بخواهیم به زبان ساده آن را توضیح بدهیم IOC تغییر کنترل می باشد یعنی بعضی از کارهای روتین را به جای این که کلاس ها انجام بدهند آن را به یک ماژول جدا بسپاریم و کلاس به کار اصلی خود برسد.
مثلا: یک کلاس که کار آن ارتباط با دیتابیس می باشد را وظیفه ایجاد اشیاء را از آن بگیریم و به یک ماژول دیگر بدهیم.
همچنین اگر بخواهیم فرق این دو را بگویم DIP یک اصل می باشد و راه کار پیاده سازی برای آن تعریف نشده است و یک مدل مفهومی می باشد اما IOC فقط یک اصل نمی باشد و راه کاری جهت پیاده سازی DIP می باشد.
انواع IOC:
1- وارونگی رابط (Interface Inversion):
شما یک برنامه ی خواندن (Reader Application) را مانند تصویر زیر در نظر بگیرید که این برنامه از یک فایل تکست میخواند.
حال در نظر بگیرید که این برنامه بخواهد از PDF و Word هم بخواند مانند تصویر زیر:
ایجاد کردن رابط های (Interface) زیاد برای هر موضوع به این معنا نمی باشد که شما اصل DIP را رعایت و یا ICO را پیاده سازی کرده اید با این کار شما ماژول های پایین دستی را به رابط ها (Interface) وابسته کرده اید و این اشتباه می باشد و هیچ سودی برای شما ندارد، حال برای این که IOC را پیاده سازی کنیم مانند زیر عمل می کنیم و رابط(Interface) را به ماژول سطح بالا که همان Reader می باشد وابسته می کنیم و همانطور که از اسم آن پیدا می باشد، ماژول های پایین دستی را در تعریف آن رابط دخیل نمی کنیم تا زمانی که بخواهیم هر نوع فایل و منبع دیگری برای خواندن مثلا دیتابیس اضافه کنیم در ماژول های بالا دستی که Reader می باشد تغییری ندهیم و فقط رابط IReader را پیاده سازی کنیم.
2- وارونگی جریان (Flow Inversion):
وارونگی جریان ساده ترین نوع IOC می باشد که میتوان گفت ستون IOC می باشد.برای مثال برنامه زیر که یک Console Application می باشد را مشاهده کنید.
همانطور که در تصویر بالا مشاهده می کنید برنامه Console می باشد که به صورت ترتیبی اطلاعات هویتی (نام، سن و... ) را از کاربر دریافت می کند که به صورت رویه ای می باشد و جریان نرمال را دارد، حال میخواهیم وارونگی جریان(Inverted Flow) را در این برنامه اعمال کنیم حال با استفاده از WindowsForm Application یک فرم به برنامه خود اضافه می کنیم مانند تصویر زیر.
همانطور که مشاهده می کنید با اضافه کردن این فرم ما جریان ورود اطلاعات را تغییر دادیم که کاربر اجباری به وارد کردن نام در گام نخست نمی باشد و می تواند سن را اول وارد کند و بعد نام را وارد کنید که این یک نمونه ساده از وارونگی جریان می باشد.
3- وارونگی خلقت(Creation Inversion):
مهترین و پرکاربرد ترین نوع IOC این نوع می باشد که از آن در IOC Container که به صورت مفصل توضیح داده خواهد شد استفاده میکنیم، برای متوجه شدن این نوع از وارونگی به مثال ها زیر توجه فرمایید:
مثال اول: در حال نرمال شما چگونه یک شی از یک کلاس می سازید:
شما در کد بالا یک شی ساخته اید که به یک کلاس دیگر وابسته می باشد که به اصطلاح، نرم افزار را Tightly coupled کرده اید، حال با استفاده از نوع Interface Inversion می خواهیم این وابستگی و مشکل را حل کنیم مانند تکه کد زیر:
همانطور که در تکه کد بالا مشاهده می کنید با ایجاد یک رابط(Interface) ایجاد شی را انجام دادیم، اما متاسفانه مشکل ما حل نشد چون هنوز وابستگی وجود دارد و با اضافه کردن رابط کار اضافه ای انجام داده ایم،ما برای حذف این وابستگی باید فرآیند ایجاد شی را از این کلاس بیرون ببریم و به یک کلاس دیگر بسپاریم که کار آن فقط ایجاد اشیاء می باشد و با انجام این کار مشکل برطرف می شود.
مثال دوم: فرض کنید برنامه ای را نوشته اید که یک دکمه(Button) دارد و بسته به تنظیمات کاربر استایل(Style) دکمه متفاوت می باشد که میتوانیم کد آن را به صورت زیر بنویسیم:
با اجرای این کد در صورتی که مطمئن باشیم در آینده هیچ استایل دیگری را نمی خواهیم به آن اضافه کنیم درست است اما حال اگر بخواهیم بعدا یک استایل دیگر به برنامه خود اضافه کنیم باید این Switch را تغییر بدهیم که این همان وابستگی این کلاس به کلاس Button می باشد که اصل DIP را نقض کرده ایم، با استفاده از Creation Inversion، ایجاد شی Button را از این کلاس جدا کنیم تا وابستگی از بین برود، برای این کار ما از الگوی طراحی FactoryMethod استفاده میکنیم که به صورت زیر می شود.
همانطور که در کلاس با مشاهده می کنید وظیفه ایجاد شی Button را یه یک کلاس دیگری به نام ButtonFactory سپرده ایم واین کلاس دیگر وابستگی ندارد و در صورتی که نیاز به اضافه کردن استایل دیگری باید در ButtonFactory تغییر بدهیم.
نمونه ای از الگوهای طراحی(Design Pattern) که این نوع از IOC را پیاده سازی و راه حل داده اند:
1- Factory Pattern
2- Service Locator
3- Dependency Injection (DI)
بیشتر اوقات برنامه نویس ها DI و IOC را با هم اشتباه می گیرند. IOC راه های متفاوتی از واگذاری مسئولیت (Inversion Of Control) همانطور که در قسمت قبل توضیح داده شد اما DI روش پیاده سازی آن نوع (Creation Inversion) را پیاده سازی می کند که این کار را با تزریق وابستگی انجام می دهد.
الگوری تزریق وابستگی شامل سه نوع کلاس زیر است:
تصویر زیر ارتباط بین سه کلاس فوق را نشان می دهد:
همانطور که مشاهده می کنید، کلاس تزریق کننده یک شیء از کلاس سرویس ایجاد می کند و آن را به کلاس کلاینت تزریق می کند. با این روش الگوی DI مسئولیت ایجاد شیء از کلاس سرویس را خارج از کلاس کلاینت انجام می دهد و در کل هدف اصلی DI حذف وابستگی های موجود می باشد و دو اصطلاح داریم:
انواع تزریق وابستگی (DI):
تزریق سازنده (Constructor Injection):
این نوع تزریق وابستگی پرکاربرد ترین نوع DI می باشد که در این نوع تزریق، کلاس Injector سرویس (dependency) را از طریق سازنده کلاس کلاینت تزریق می کند.
در مثال زیر الگوی تزریق وابستگی با استفاده از Constructor پیاده سازی شده است:
در کد بالا ConstructorInjectionWalletService شامل سازنده ای با پارامتری از نوع IWalletRepository است و جهت استفاده از این کلاس باید به Constructor این کلاس شی مورد نظر داده شود که به آن تزریق می گویند.
تزریق پراپرتی (Property Injection):
در مثال زیر الگوی تزریق وابستگی با استفاده از Property پیاده سازی شده است:
در کد بالا، کلاس PropInjectionWalletService شامل یک پراپرتی عمومی به نام walletRepository_ است که برای تنظیم یک شیء پیاده سازی شده بر اساس IWalletRepository استفاده می شود و هر کلاسی بخواهد از آن استفاده کند باید اول پراپرتی را پر کند.
تزریق متد (Method Injection):
در این نوع تزریق، کلاس کلاینت برای مشخص کردن وابستگی ها یک interface پیاده سازی می کند و کلاس تزریق کننده با استفاده از این Interface وابستگی های کلاس کلاینت را تزریق می کند.
در مثال فوق، کلاس MethodInjectionWalletService رابط IWalletServiceDependency را که شامل متد SetDependency است را پیاده سازی کرده است. بنابراین کلاس تزریق کننده می تواند با استفاده از متد SetDependency کلاسی که اینترفیس IWalletRepository را پیاده سازی کرده است را به تزریق کند.
حال هر سه نوع تزریق وابستگی را متوجه شده اید، نکته ای که قابل تعمل می باشد که ما فقط وابستگی کلاس ها رو حذف کردیم و با استفاده از روش های بالا وابستگی را تزریق کردیم و باز هم جهت سهولت ایجاد اشیا در کلاس های بالا دستی از IOC Container استفاده می کنیم که در پایین به صورت کامل توضیح داده شده است.
حال که مفاهیم و راه حل های بالا را به خوبی درک کرده اید،می خواهیم ببینیم ICO Container چه می باشد:
آیا هنوز این موضوع برای شما روشن نشده است؟ ICO Container فریم ورکی است که برای ایجاد وابستگی و تزریق آن زمانی که نیاز دارید می باشد، به این معنی که شما نیازی نیست به صورت دستی این وابستگی را ایجاد و آن را تزریق کنید بلکه این فریم ورک این کار را برای شما انجام می دهد.
این فریم ورک ها چه کاری را انجام می دهند:
فریم ورک های زیادی وجود دارند که بهترین آن ها:
بسته به نیاز خود می توانید هر کدام از این فریم ورک ها را به پروژه خود اضافه کنید و استفاده کنید.
اگر خواهیم چهار مفهوم بالا را در یک عکس ببینیم به صورت زیر می باشد که می توانیم تفاوت آن ها را با هم از روی این عکس متوجه بشویم.
جمع بندی:
در این نوشته سعی کردم به صورت مفهومی IOC, DIP, DI, IOC Container را توضیح بدم و امیدوارم مفید باشه براتون.دوستان عزیز توصیه میکنم که کد های پیاده سازی شده این نوشته را از گیت هاب دریافت کرده و آن را روی سیستم خود اجرا و دیباگ کنید. در نهایت اگر سوال یا نظری داشتید، خوشحال میشوم که آن را در بخش نظرات این نوشته مطرح کنید.