محسن اسدی
محسن اسدی
خواندن ۱۲ دقیقه·۳ سال پیش

IOC - Inversion Of Control

به نام خدا


در این نوشته می خواهم در مورد چند مفهوم در طراحی نرم افزار توضیح بدهم:

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
حال می توانیم موراد بالا را مطابق تصویر زیر دسته بندی کنیم:


DIP (Dependency Inversion Principle)

این موضوع را با تعریف رابرت مارتین (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) بین ماژول بالا دستی و ماژول پایین دستی این اصل را پیاده سازی و رعایت کردیم که با رعایت این اصل ما چه در بخش خواندن از (کیبورد، اسکنر و... ) و نوشتن در(فایل تکیت، پرینتر، دیتابیس و...) را اضافه کنیم بدون این که تغییری در ماژول بالادستی بخواهیم انجام دهیم، انجام خواهد شد.


IOC (Inversion Of Control)

در قسمت بالا DIP به ما توضیحی نمی دهد که مشکل عدم رعایت این اصل را چگونه پیاده سازی کنیم اما وارونگی کنترل (IOC) و یا دیگر معنی آن (واگذاری مسئولیت)، راه های رسیدن به DIP را به ما می گوید، تعاریف زیادی برای این موضوع وجود دارد اما میتوان گفت که این ساده ترین تعریف می باشد.

اگر بخواهیم به زبان ساده آن را توضیح بدهیم IOC تغییر کنترل می باشد یعنی بعضی از کارهای روتین را به جای این که کلاس ها انجام بدهند آن را به یک ماژول جدا بسپاریم و کلاس به کار اصلی خود برسد.
مثلا: یک کلاس که کار آن ارتباط با دیتابیس می باشد را وظیفه ایجاد اشیاء را از آن بگیریم و به یک ماژول دیگر بدهیم.

همچنین اگر بخواهیم فرق این دو را بگویم DIP یک اصل می باشد و راه کار پیاده سازی برای آن تعریف نشده است و یک مدل مفهومی می باشد اما IOC فقط یک اصل نمی باشد و راه کاری جهت پیاده سازی DIP می باشد.


انواع IOC:

  • 1- Interface Inversion
  • 2- Flow Inversion
  • 3- Creation Inversion

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 که به صورت مفصل توضیح داده خواهد شد استفاده میکنیم، برای متوجه شدن این نوع از وارونگی به مثال ها زیر توجه فرمایید:

مثال اول: در حال نرمال شما چگونه یک شی از یک کلاس می سازید:

https://gist.github.com/mohsenasadi501/fb17f42a6ca15c020215dc93763e3d3a

شما در کد بالا یک شی ساخته اید که به یک کلاس دیگر وابسته می باشد که به اصطلاح، نرم افزار را Tightly coupled کرده اید، حال با استفاده از نوع Interface Inversion می خواهیم این وابستگی و مشکل را حل کنیم مانند تکه کد زیر:

https://gist.github.com/mohsenasadi501/35b62ed1eead0d851875b9f12f6db6bb

همانطور که در تکه کد بالا مشاهده می کنید با ایجاد یک رابط(Interface) ایجاد شی را انجام دادیم، اما متاسفانه مشکل ما حل نشد چون هنوز وابستگی وجود دارد و با اضافه کردن رابط کار اضافه ای انجام داده ایم،ما برای حذف این وابستگی باید فرآیند ایجاد شی را از این کلاس بیرون ببریم و به یک کلاس دیگر بسپاریم که کار آن فقط ایجاد اشیاء می باشد و با انجام این کار مشکل برطرف می شود.

مثال دوم: فرض کنید برنامه ای را نوشته اید که یک دکمه(Button) دارد و بسته به تنظیمات کاربر استایل(Style) دکمه متفاوت می باشد که میتوانیم کد آن را به صورت زیر بنویسیم:

https://gist.github.com/mohsenasadi501/fa770ca4865b21aa679d809c8fbbbd36

با اجرای این کد در صورتی که مطمئن باشیم در آینده هیچ استایل دیگری را نمی خواهیم به آن اضافه کنیم درست است اما حال اگر بخواهیم بعدا یک استایل دیگر به برنامه خود اضافه کنیم باید این Switch را تغییر بدهیم که این همان وابستگی این کلاس به کلاس Button می باشد که اصل DIP را نقض کرده ایم، با استفاده از Creation Inversion، ایجاد شی Button را از این کلاس جدا کنیم تا وابستگی از بین برود، برای این کار ما از الگوی طراحی FactoryMethod استفاده میکنیم که به صورت زیر می شود.

https://gist.github.com/mohsenasadi501/1d6de9bc2d2d86ccc680de67db7b9905

همانطور که در کلاس با مشاهده می کنید وظیفه ایجاد شی Button را یه یک کلاس دیگری به نام ButtonFactory سپرده ایم واین کلاس دیگر وابستگی ندارد و در صورتی که نیاز به اضافه کردن استایل دیگری باید در ButtonFactory تغییر بدهیم.

نمونه ای از الگوهای طراحی(Design Pattern) که این نوع از IOC را پیاده سازی و راه حل داده اند:

1- Factory Pattern

2- Service Locator

3- Dependency Injection (DI)


Dependency Injection (DI)

بیشتر اوقات برنامه نویس ها DI و IOC را با هم اشتباه می گیرند. IOC راه های متفاوتی از واگذاری مسئولیت (Inversion Of Control) همانطور که در قسمت قبل توضیح داده شد اما DI روش پیاده سازی آن نوع (Creation Inversion) را پیاده سازی می کند که این کار را با تزریق وابستگی انجام می دهد.

الگوری تزریق وابستگی شامل سه نوع کلاس زیر است:

  • کلاس کلاینت (Client Class): کلاسی می باشد که به کلاس سرویس(Service Class) وابسته است.
  • کلاس سرویس (Service Class): کلاسی می باشد که خدماتی را به کلاس کلاینت ارائه می کند.
  • کلاس تزریق کننده (Injector Class): کلاسی است که شیء از کلاس سرویس را به کلاس کلاینت تزریق می کند.

تصویر زیر ارتباط بین سه کلاس فوق را نشان می دهد:

همانطور که مشاهده می کنید، کلاس تزریق کننده یک شیء از کلاس سرویس ایجاد می کند و آن را به کلاس کلاینت تزریق می کند. با این روش الگوی DI مسئولیت ایجاد شیء از کلاس سرویس را خارج از کلاس کلاینت انجام می دهد و در کل هدف اصلی DI حذف وابستگی های موجود می باشد و دو اصطلاح داریم:

  • کمترین وابستگی (Loose Coupled):
    این اصطلاح برای زمانی می باشد که کمترین وابستگی وجود دارد که از بین بردن این واستگی با استفاده از DI و روش های گفته شده انجام می شود.
  • بیشترین وابستگی (Tight Coupled):
    این اصطلاح برای زمانی می باشد که بیشترین وابستگی بین لایه های برنامه و حتی بین کلاس های می باشد.

انواع تزریق وابستگی (DI):

تزریق سازنده (Constructor Injection):
این نوع تزریق وابستگی پرکاربرد ترین نوع DI می باشد که در این نوع تزریق، کلاس Injector سرویس (dependency) را از طریق سازنده کلاس کلاینت تزریق می کند.

در مثال زیر الگوی تزریق وابستگی با استفاده از Constructor پیاده سازی شده است:

https://gist.github.com/mohsenasadi501/00c152816e67dd1d0b6fe38a83ff1635

در کد بالا ConstructorInjectionWalletService شامل سازنده ای با پارامتری از نوع IWalletRepository است و جهت استفاده از این کلاس باید به Constructor این کلاس شی مورد نظر داده شود که به آن تزریق می گویند.


تزریق پراپرتی (Property Injection):
در مثال زیر الگوی تزریق وابستگی با استفاده از Property پیاده سازی شده است:

https://gist.github.com/mohsenasadi501/437be09d441f9bcdd47b8277e8838d34

در کد بالا، کلاس PropInjectionWalletService شامل یک پراپرتی عمومی به نام walletRepository_ است که برای تنظیم یک شیء پیاده سازی شده بر اساس IWalletRepository استفاده می شود و هر کلاسی بخواهد از آن استفاده کند باید اول پراپرتی را پر کند.

تزریق متد (Method Injection):
در این نوع تزریق، کلاس کلاینت برای مشخص کردن وابستگی ها یک interface پیاده سازی می کند و کلاس تزریق کننده با استفاده از این Interface وابستگی های کلاس کلاینت را تزریق می کند.

https://gist.github.com/mohsenasadi501/8ab652e524f40968568d0961b112a8f7

در مثال فوق، کلاس MethodInjectionWalletService رابط IWalletServiceDependency را که شامل متد SetDependency است را پیاده سازی کرده است. بنابراین کلاس تزریق کننده می تواند با استفاده از متد SetDependency کلاسی که اینترفیس IWalletRepository را پیاده سازی کرده است را به تزریق کند.


حال هر سه نوع تزریق وابستگی را متوجه شده اید، نکته ای که قابل تعمل می باشد که ما فقط وابستگی کلاس ها رو حذف کردیم و با استفاده از روش های بالا وابستگی را تزریق کردیم و باز هم جهت سهولت ایجاد اشیا در کلاس های بالا دستی از IOC Container استفاده می کنیم که در پایین به صورت کامل توضیح داده شده است.


IOC Container

حال که مفاهیم و راه حل های بالا را به خوبی درک کرده اید،می خواهیم ببینیم ICO Container چه می باشد:

  • فریم ورکی(Framework) برای تزریق وابستگی
  • فراهم آوردن راهکاری برای مدیریت وابستگی ها
  • برطرف کردن اتوماتیک وابستگی های برنامه

آیا هنوز این موضوع برای شما روشن نشده است؟ ICO Container فریم ورکی است که برای ایجاد وابستگی و تزریق آن زمانی که نیاز دارید می باشد، به این معنی که شما نیازی نیست به صورت دستی این وابستگی را ایجاد و آن را تزریق کنید بلکه این فریم ورک این کار را برای شما انجام می دهد.

این فریم ورک ها چه کاری را انجام می دهند:

  • وابستگی بین اشیاء را برای ما ایجاد می کند
  • وابستگی ها را برای ما تزریق می کند، زمانی که نیاز داریم
  • طول عمر اشیاء (Object Lifetime) را برای ما مدیریت می کند
  • همچنین کارهای خیلی زیاد دیگری را هم انجام می دهد …

فریم ورک های زیادی وجود دارند که بهترین آن ها:

  • 1- Autofac
  • 2- Simple Injector
  • 3- StructureMap
  • 4- Ninject
  • 5- Unity
  • 6- Castle Windsor
  • 7- .NetCore Build-in IOC

بسته به نیاز خود می توانید هر کدام از این فریم ورک ها را به پروژه خود اضافه کنید و استفاده کنید.

اگر خواهیم چهار مفهوم بالا را در یک عکس ببینیم به صورت زیر می باشد که می توانیم تفاوت آن ها را با هم از روی این عکس متوجه بشویم.

جمع بندی:
در این نوشته سعی کردم به صورت مفهومی IOC, DIP, DI, IOC Container را توضیح بدم و امیدوارم مفید باشه براتون.دوستان عزیز توصیه میکنم که کد های پیاده سازی شده این نوشته را از گیت هاب دریافت کرده و آن را روی سیستم خود اجرا و دیباگ کنید. در نهایت اگر سوال یا نظری داشتید، خوشحال میشوم که آن را در بخش نظرات این نوشته مطرح کنید.

https://github.com/mohsenasadi501/IOCSample


diiocdipتزریق وابستگی
تحلیلگر، توسعه دهنده ارشد نرم افزار
شاید از این پست‌ها خوشتان بیاید