وارونگی وابستگی (Dependency Injection)، تزریق وابستگی (Dependency Inversion) و وارونگی کنترل (Inversion of Control) 3 اصطلاحی هستند که اگرچه مرتبط هستند، اما معمولا اشتباه گرفته می شوند و به اشتباه تفسیر می شوند. در اینترنت میتوانید چندین مقاله و پستهای وبلاگی را بیابید که آنها را یکسان نشان میدهند، یا آنها را به شیوهای نادرست مرتبط میکنند و باعث سردرگمی و ابهام میشوند که بیپایان تکرار شوند.
در آنوفل ما به شدت از اصول و الگوهای طراحی مناسب پیروی می کنیم، این تضمین می کند که کد ما دارای سطحی از توسعه پذیری و کیفیت مقاوم در آینده است که با جاه طلبی های بلندمدت ما مطابقت دارد و از رشد فوق العاده ما پشتیبانی می کند.
در این مقاله، من سعی میکنم هر کدام را به صورت جدا گونه توضیح دهم، تاریخچهای به شما بدهم و دلیل وجود این تصورات نادرست برای این 3 مفهوم مهم را توضیح دهم.
وارونگی کنترل (IoC)
وارونگی کنترل یک اصل برنامه نویسی است که جریان کنترل را در یک برنامه معکوس می کند. در برنامهنویسی رویهای سنتی، کدی که اجرای برنامه را کنترل میکند، تابع اصلی اشیا را نمونهسازی میکند، متدها را فراخوانی میکند و حتی از کاربر ورودی میخواهد تا اجرا ادامه یابد و برنامه بتواند به وظیفه خود برسد. با IoC، این چارچوبی است که نمونهسازی، فراخوانیهای متد و راهاندازی اقدامات کاربر را انجام میدهد، کنترل کامل جریان را دارد و این مسئولیت را از عملکرد اصلی و در نتیجه برنامه حذف میکند.
مفهوم وارونگی کنترل اولین بار در سال 1988 توسط رالف ای جانسون و برایان فوت در مقاله طراحی کلاس های قابل استفاده مجدد معرفی شد، جایی که آنها بیان می کنند:
یکی از ویژگیهای مهم یک فریم ورک این است که متد هایی که کاربر برای تنظیم چارچوب تعریف میکند، اغلب از درون خود چارچوب فراخوانی میشود، نه از کد برنامه کاربر. فریمورک اغلب نقش برنامه اصلی را در هماهنگی و توالی فعالیت برنامه ایفا می کند. این وارونگی کنترل به چارچوب ها این قدرت را می دهد که به عنوان اسکلت های توسعه پذیر عمل کنند. روشهای ارائهشده توسط کاربر، الگوریتمهای عمومی تعریفشده در چارچوب را برای یک برنامه خاص تنظیم میکنند.
سپس، در سال 1996، مایکل متسون از اصطلاح IoC در پایان نامه خود "چارچوب های شی گرا: بررسی مسائل متد شناختی" برای متمایز کردن یک چارچوب واقعی از یک کتابخانه کلاسی با بیان این جمله استفاده می کند:
تفاوت عمده بین یک چارچوب شی گرا و یک کتابخانه کلاس این است که فریمورک کد برنامه را فراخوانی می کند. معمولاً کد برنامه کتابخانه کلاس را فرا می خواند. این وارونگی کنترل گاهی اوقات به عنوان اصل هالیوود نامیده می شود، "به ما زنگ نزنید، ما با شما تماس می گیریم".
سرانجام، در سال 1998، با پیشنهاد Java Apache Server Framework یا Avalon، Stefano Mazzocchi از وارونگی کنترل به عنوان یکی از اصلیترین اصول طراحی محرک چارچوب استفاده کرد و این مفهوم را رایج کرد.
به عنوان مثال : فرض کنید یک کلاس کنترلر وجود دارد که به نمونه ای از کلاس سرویس نیاز دارد. یک رویکرد این است که خود کنترل کننده سرویس را نمونه سازی می کند و از آن استفاده می کند. همانطور که شما می خواهید خوب کار می کند. در این مورد، سرویس یک وابستگی کنترل کننده است. اما اکنون همانطور که می بینید تست کنترلر سخت است زیرا هیچ راهی وجود ندارد که بتوانید سرویس را ماک کنید. اگر سرویس را ماک نکنید، همچنان میتوانید کنترلکننده را تست کنید، اما متد هایی سرویس واقعی را فراخوانی میکند که ممکن است چالشهای بلادرنگ دیگری را به همراه داشته باشد، اگر خود سرویس به برخی وابستگیهای دیگر مانند پایگاه داده و غیره متکی باشد. ما باید بتوانیم کنترلر را تست کنیم. بدون پایگاه داده یا حتی سرویس واقعی. شما در حال تست Controller هستید نه سرویس. بنابراین برای رسیدگی به این موضوع، تصمیم میگیرید شیء سرویس را خارج از کنترلر ایجاد کنید (آن را از یک چارچوب DI یا یک سرویس یاب یا نمونهسازی دستی دریافت کنید) و شیء سرویس را به سازنده کنترلر ارسال کنید. کنترلر همچنان می تواند از این سرویس برای تمام اهدافی که به آن نیاز دارد استفاده کند.
2 اتفاق اینجا افتاد:
قبل از این، Controller کنترل نمونه سازی شی سرویس را بر عهده داشت، اما اکنون یک کلاس دیگر سرویس را نمونه سازی می کند، کنترل معکوس شده است - Inversion of Control.
شما وابستگی (شیء سرویس) را به Controller ارسال کردید، این الگو به عنوان تزریق وابستگی شناخته می شود. که قرار است کمی در مورد آن صحبت کنم. یک مزیت ملموس این است که می توان یک شیء سرویس ساختگی ایجاد کرد و به کنترلر منتقل کرد و آن را به راحتی قابل آزمایش کرد.
در زمینه کانتینرهای سرویس، IoC با اجازه دادن به چارچوب برای انجام اتصال و نمونه سازی وابستگی ها به دست می آید. به جای اینکه برنامه مجبور باشد خدماتی را ایجاد و استفاده کند، این چارچوب است که با استفاده از یک پیکربندی از پیش تعیین شده که نحوه اجرای این وظایف را به آن آموزش میدهد، زمان مورد نیاز نمونهسازی را تعیین میکند.
مثال در PHP :
interface PaymentContract
{
public function removeSubscription();
public function payNow();
}
class PaymentGateWay
{
protected $gateway;
public function __construct(PaymentContract $gateway)
{
$this->gateway = $gateway;
}
public function charge()
{
$this->gateway->payNow();
}
}
class Stripe implements PaymentContract
{
protected $subscription;
public function __construct(StripeSubsciption $subscription)
{
$this->subscription = $subscription;
}
public function removeSubscription()
{
$this->subscription->cancelSubscription();
}
public function payNow()
{
//do stuff
}
}
class StripeSubscription
{
public function createSubscription()
{
//do stuff
}
public function cancelSubscription()
{
//do stuff
}
}
class ChargeBee implements PaymentContract
{
protected $subscription;
public function __construct(ChargeBeeSubscription $subscription)
{
$this->subscription = $subscription;
}
public function removeSubscription()
{
$this->subscription->cancelSubscription();
}
public function payNow()
{
//do stuff
}
}
class ChargeBeeSubscription
{
public function createSubscription()
{
//do stuff
}
public function cancelSubscription()
{
//do stuff
}
}
/** --------------------------------------------
$subscription = new ChargeBeeSubscription();
$chargeBee = new ChargeBee($subscription);
$gateway = new PaymentGateWay($chargeBee);
--------------------------------------------- **/
//now we change the service from ChargeBee to Stripe
$subscription = new StripeSubscription();
$stripe = new Stripe($subscription);
$gateway = new PaymentGateWay($stripe);
تزریق وابستگی (DI)
Dependency Injection یک تکنیک طراحی نرم افزاری است که در آن ایجاد و اتصال وابستگی ها خارج از کلاس وابسته انجام می شود. پس از آن، وابستگی های گفته شده از قبل به صورت نمونه و آماده برای استفاده ارائه می شوند، از این رو اصطلاح "تزریق" نامیده می شود. برخلاف کلاس وابسته که مجبور است وابستگی های خود را به صورت داخلی نمونه سازی کند، و باید بداند که چگونه آنها را پیکربندی کند، در نتیجه باعث جفت شدن می شود. برای آشنایی بیشتر با ترزیق وابستگی این مقاله را بررسی کنید.
اگر پاراگراف قبلی را اضافی نسبت به پاراگراف قبل از آن یافتید، تصادفی نیست. Dependency Injection اسمی بود که توسط مارتین فاولر در سال 2004 ابداع شد تا اسم بهتر و خاصتر برای این سبک داشته باشد، برخلاف اصطلاح بسیار عمومی Inversion of Control که توسط بسیاری از چارچوبها استفاده میشود.
DI را می توان به 3 روش به دست آورد:
Constructor Injection: زمانی که وابستگی ها از طریق سازنده کلاس وابسته ارائه می شود
Interface Injection: زمانی که وابستگی ها مستقیماً در یک متد کلاس وابسته به عنوان آرگومان ارائه می شوند
Setter Injection: زمانی که وابستگی ها از طریق یک ویژگی عمومی از کلاس وابسته ارائه می شوند
صرف نظر از اینکه چه نوع تزریقی استفاده میشود، این الگو ساختار و پیکربندی سرویسها را از کاربرد آن جدا میکند، کلاس وابسته را از مسئولیت قبلی خلاص میکند، آن را از وابستگیهایش جدا میکند و قابلیت استفاده مجدد و سهولت آزمایش را بهبود میبخشد.
واقعیت جالب: استفانو مازوکی به شدت با ایده مارتین فاولر مبنی بر تغییر نام Inversion of Control به Dependency Injection مخالف بود و اظهار داشت که او "نقطه را از دست داده است" و "IoC در مورد اعمال انزوا است نه برای تزریق وابستگی".
برای آشنایی با تزریق وابستگی و سرویس کانتینر در لاراول این مقاله را بررسی کنید.
الگوهای دیگری نیز وجود دارد که میتوان وابستگیها را پیکربندی کرد، نمونهسازی کرد و در اختیار یک کلاس قرار داد تا از آن استفاده کند، بهعنوان مثال دیزاین پترن Service Locator، که در آن کلاسها به نمونه Service Locator بستگی دارند که وابستگیها را بر اساس فرمان سیمکشی و ارائه میکند. تفاوت، دوباره، وابستگی است. Service Locator همه کلاسها را مجبور میکند تا در مورد نمونه Service Locator بدانند. برخلاف این، DI نیازی به وابستگی کلاس کلاینت به کانتینر سرویس ندارد، وابستگی ها به سادگی آماده استفاده به نظر می رسند و به طور موثر وارونگی کنترل را به دست می آورند.
مثال در PHP :
class PaymentGateWay
{
protected $gateway;
public function __construct(ChargeBee $chargeBee)
{
$this->gateway = $chargeBee;
}
public function charge()
{
$this->gateway->payNow();
}
}
class ChargeBee
{
protected $subscription;
public function __construct(ChargeBeeSubscription $subscription)
{
$this->subscription = $subscription;
}
public function removeSubscription()
{
$this->subscription->cancelSubscription();
}
public function payNow()
{
//do stuff
}
}
class ChargeBeeSubscription
{
public function createSubscription()
{
//do stuff
}
public function cancelSubscription()
{
//do stuff
}
}
$subscription = new ChargeBeeSubscription();
$chargeBee = new ChargeBee($subscription);
$gateway = new PaymentGateWay($chargeBee);
در کد بالا، کلاس ChargeBee
از کلاس ChargeBeeSubscription
استفاده می کند.
به این معنی که اگر می خواهید شی ChargeBee
را ایجاد کنید، به ChargeBeeSubscription
Object نیاز داریم. مانند PaymentGateWay
و کلاس ChargeBee
. اگر قبل از آن یک شی از PaymentGateWay
ایجاد کنید، باید یک شی ChargeBee
ایجاد کنیم.
ما در حال تزریق شی ChargeBeeSubscription
به سازنده ChargeBee
و شی ChargeBee
به سازنده PaymentGateWay
هستیم.
ممکن است فکر کنید هی اینجا ChargeBee
به شدت به PaymentGateWay
متصل است. اگر بخواهم روند پرداخت خود را به Stripe
تغییر دهم چه می شود. فعلاً نگران این موضوع نباشید، اینجاست که IOC به تصویر کشیده می شود.
برای آشنایی با تزریق وابستگی در Node.js می توانید این مقاله را بررسی کنید.
اصل وارونگی وابستگی (DIP)
اصل وارونگی وابستگی توسط رابرت مارتین، با نام مستعار عمو باب، در ابتدا در مقاله خود معیارهای کیفیت طراحی OO در سال 1994 شد، و بعداً در کتاب اصول، الگوها و شیوههای توسعه نرمافزار چابک در سال 2002 نامگذاری و تعریف شد. برای آشنایی با اصول سالید این مقاله را بررسی کنید. با استفاده از 2 نکته زیر:
ماژول های سطح بالا نباید به ماژول های سطح پایین وابسته باشند. هر دو باید به abstractions بستگی داشته باشند.
abstraction ها نباید به جزئیات بستگی داشته باشند. جزئیات باید به abstractions بستگی داشته باشد.
بیایید سعی کنیم این تعریف را به چیزی کاربردی تر ترجمه کنیم.
یک ماژول سطح بالا، در این زمینه، کلاسی است که به یک سرویس وابسته است، بنابراین، کلاینت سرویس است. این ماژول سطح بالا است که شامل تصمیم مهم خط مشی و منطق تجاری یک برنامه کاربردی است. ماژول سطح پایین کلاسی است که عملکرد خاصی را به کلاینت خود ارائه می دهد، بنابراین سرویس است.
اگر یک کلاینت به اجرای یک سرویس وابسته باشد، آنگاه وقتی رابط سرویس تغییر می کند، در نتیجه کلاینت باید تغییر کند (و همچنین تست های آن). اگر این وابستگی را معکوس کنیم تا ماژول سطح بالا رابطی را که ماژول سطح پایین باید پیاده سازی کند تعیین کند، ماژول سطح پایین می تواند تغییر کند تا زمانی که قرارداد آن همیشه رعایت شود.
مهم است که توجه داشته باشید که نه تنها وارونگی جهت وابستگی، بلکه وارونگی در مالکیت رابط نیز وجود دارد.
جریان وابستگی زیر DIP را نقض می کند زیرا ماژول های سطح بالا به ماژول های سطح پایین بستگی دارند.
در عوض، ما باید انتزاعات اساسی را پیدا کنیم، یا به قول عمو باب باید پیدا کنیم:
حقایقی که با تغییر جزئیات فرق نمی کنند. این سیستم درون سیستم است، استعاره است.
بیایید با معکوس کردن وابستگی و وابستگی جزئیات به abstractions آن را برطرف کنیم.
در این حالت، جهت وابستگی معکوس می شود، واسط ها در سطح بالاتری تعیین می شوند و کلاس های همان سطح به آنها بستگی دارند، بنابراین به abstraction ها بستگی دارند. علاوه بر این، پیادهسازی کلاسهای سطح پایین به رابطهای تعریفشده در سطح بالاتر بستگی دارد، بنابراین جزئیات اکنون به abstractions بستگی دارد.
اگر کلاینت و خدمات هر دو به یک abstraction بستگی دارند، در این صورت اساساً توافق نامه ای دارید که اگر رعایت شود، موارد زیر را مجاز می کند:
کلاینت نسبت به اینکه وابستگی چیست، ناشناس خواهد بود و در عوض به کاری که انجام می دهد تکیه می کند
وابستگی تضمین خواهد شد که به گونه ای رفتار کند که توسط سیاست های سطح بالا تعیین می شود
کلاینت می تواند با خیال راحت در زمینه های دیگر مورد استفاده مجدد قرار گیرد، با اعتماد به اینکه وابستگی هایش به قرارداد احترام می گذارند
وابستگی را همیشه می توان با اجرای دیگری جایگزین کرد که همان قرارداد را اجرا می کند
همانطور که می بینید هر سه مفهوم با هم مرتبط هستند و در یک زمینه، ساخت برنامه ها و استفاده از چارچوب ها وجود دارند، اما صرف نظر از روابط و شباهت های آنها، دانستن جزئیات آنها و مزایای استفاده از آنها مهم است.
نتیجه
وارونگی کنترل یک اصل اساسی است که توسط فریمورک ها برای معکوس کردن مسئولیت های کنترل جریان در یک برنامه کاربردی استفاده می شود، در حالی که Dependency Injection الگویی است که برای ارائه وابستگی ها به کلاس برنامه استفاده می شود. و برای اینکه کلاس و سرویسهای آن به درستی جدا شوند، اصل وابستگی وارونگی باید توسط کلاینت و سرویس مورد احترام قرار گیرد، همیشه بسته به یک abstraction.