رفتن به نوشته‌ها

آموزش تزریق وابستگی‌ ها (Dependency Injection) + مثال کاربردی

تزریق وابستگی یا Dependency Injection یک پترن و الگوی طراحی است که هدف اصلی آن حذف وابستگی‌‌های موجود بین دو کلاس با استفاده از یک رابط (Interface) است. در تزریق وابستگی‌ها دو اصطلاح داریم.

  1. loosely coupled: بدین معنیست که کمترین وابستگی بین دو کلاس وجود داشته باشد.
  2. tight coupled: بدین معنیست که بیشترین وابستگی بین دو کلاس وجود داشته باشد.

بنابراین باید برنامه و نرم‌افزار خود را طوری طراحی کنیم که تا حد ممکن استحکام ضعیف (loosely coupled) باشد. هنگامی‌که دو کلاس tight coupled هستند (یعنی به همدیگر وابستگی بسیار دارند یا به اصطلاح دیگری در هم چفت شده‌اند) می‌توان گفت با استفاده از یک رابطه‌ی باینری به یکدیگر متصل هستند و این امر انعطاف‌پذیری نرم‌افزار را به شدت پایین می‌آورد.

قبل از ادامه‌ی مبحث تزریق وابستگی یک پیش‌زمینه به شما ارائه خواهیم داد:

پیش‌‌زمینه

قبل از اینکه درباره‌ی Dependency injection صحبت کنیم ابتدا باید مشکل را بدانید تا بخواهید آن را با استفاده از DI یا Dependency Injection حل کنید. برای فهمیدن این مشکل ابتدا باید دو مفهوم را بیان کنیم:

  1. اصل وارونگی وابستگی (Dependency Inversion Principle یا به اختصار DIP)
  2. وارونگی کنترل (Inversion of Controls یا به اختصار IoC)

مطالعه‌‌ی این مفاهیم شمارا در درک DI کمک می کند. بنابراین با دقت تمام مطالعه بفرمایید.

Dependency Inversion Principle

اصل وارونگی وابستگی، یک اصل طراحی نرم‌افزار است که به ما در تولید یک نرم‌افزار با استحکام ارتباطی کم (Loosely Coupled) کمک می کند. بر اساس این تعریف اصول وارونگی وابستگی عبارتند از:

  1. ماژول‌های سطح بالا نباید به ماژول‌های سطح پایین وابسته باشند. بلکه باید هر دو به یک رابط وابسته شوند.
  2. انتزاع‌ (abstraction) نباید به جزئیات وابسته باشند. جزئیات باید به انتزاع وابسته باشند. (منظور از انتزاع دید کلی نسبت به یک شیء است مثلا وقتی ما می‌گوییم میز چیزی که در ذهن ما نقش می‌بندد یک شکل کلی است ولی وقتی می‌گوییم میز ناهارخوری دقیقا مشخص می‌کنیم که چه نوع میزی است. در نتیجه انتزاع یک دید کلی از یک شیء بحساب می‌آید)

این دو اصل چه چیزی را مطرح می‌کنند؟ اجازه بدهید تا مفاهیم را با طرح یک مثال شرح دهیم: بنده در سال‌های گذشته می‌خواستم نرم‌افزار ویندوزی بنویسم که توسط آن بتوانم یک وب سرور را اجرا کنم. اما حین کدنویسی این نرم‌افزار به مشکلاتی برخورد کردم. تنها وظیفه‌ای که این نرم‌افزار به عهده داشت گزارش پیام‌های خطا به هنگام اتصالات IIS و درج آنها در گزارشات رویدادها بود. خب تیم ما برای انجام این کار ابتدا ۲ کلاس ایجاد کرد. یکی برای مانیتورینگ نرم‌افزار و دیگری برای نوشتن پیام‌ها در صفحه گزارشات. این دو کلاس به صورت زیر تعریف شده بودند:

class EventLogWriter
{
    public void Write(string message)
    {
        //Write to event log here
    }
}

class AppPoolWatcher
{
    // تنظیم کردن کلاس برای نوشتن پیام‌ها
    EventLogWriter writer = null;

    // این متد مشکلات را یادداشت می‌کرد
    public void Notify(string message)
    {
        if (writer == null)
        {
            writer = new EventLogWriter();
        }
        writer.Write(message);
    }
}

در نگاه اول، طراحی کلاس‌های فوق مناسب به نظر می‌رسند. به‌گونه‌ایست که فکر می‌کنید کدها بسیار عالی هستند! اما اینجا یک مشکل وجود دارد و آن نقض اصل شماره ۱ وارونگی وابستگی‌ست. یعنی ماژول سطح بالای AppPoolWatcher به ماژول سطح پایین EventLogWriter وابسته است. حال این مشکل را اینجا نگه دارید.

در ادامه نرم‌افزار باید مجموعه‌ای از خطاها را با اتصال به اینترنت به ایمیل مدیریت شبکه ارسال می‌کرد. حال چطور باید این کار را انجام می‌داد؟ یک راه ایجاد یک کلاس برای ارسال ایمیل‌ها و قرار دادن در کلاس AppPoolWatcher بود اما در هر لحظه ما می‌توانیم تنها از یک شیء برای انجام عملیات استفاده کنیم یا EventLogWriter یا EmailSender. این مشکل هنگامیکه میخواستیم فرمان‌های بیشتری مانند ارسال SMS و … در نرم‌افزار قرار دهیم، بیشتر به چشم می‌خورد. بنابراین ما کلاس هایی را در اختیار داشتیم که نمونه‌های زیادی درون AppPoolWatcher اضافه می‌کردند و وابستگی هرچه تمام این نمونه‌ها به ماژول سطح بالای AppPoolWatcher باعث می‌شد که قانون اول اصل وارونگی وابستگی برهم بخورد. اما سوال اینجاست که چگونه این مشکل را برطرف کنیم!؟ پاسخ Inversion of Control است.

Inversion of Control یا IoC

همانطور که ملاحظه کردید اصل وارونگی وابستگی به ما می‌گوید که وابستگی دو ماژول باید چگونه باشد. برای اجرای این موضوع باید از IoC یا وارونگی کنترل استفاده کنیم. IoC روشی کاملا کاربردی‌ است که توسط آن ماژول‌های سطح بالا را به جای وابسته کردن به ماژول‌های سطح پایین، به انتزاع‌ها (abstractions) متصل می‌کند. حال برای برطرف کردن مشکل در مثال فوق باید ابتدا یک انتزاع (abstraction) ایجاد کرد که ماژول سطح بالا به آن وابسته باشد. بنابراین یک رابط (Interface) که منجر به تولید یک انتزاع (abstraction) می‌شود، تعریف می‌کنیم:

public interface INofificationAction
{
    public void ActOnNotification(string message);
}

حال اجازه بدهید ماژول سطح بالا را تغییر دهیم. یعنی AppPoolWatcher از انتزاع (abstraction) به جای ماژول سطح پایین EventLogwriter‌ استفاده کند. بنابراین داریم:

class AppPoolWatcher
{
    // تنظیم کردن انتزاع برای انجام عملیات
    INofificationAction action = null;

    // این تابع در صورت وجود مشکل گزارش خواهد داد
    public void Notify(string message)
    {
        if (action == null)
        {
            // اینجا باید از یک کلاس رابط استفاده کنیم. 
        }
        action.ActOnNotification(message);
    }
}

و حال باید تغییراتی در ماژول سطح پایین ایجاد کرده تا رابطه‌ی بین کلاس سطح پایین EventLogWriter و انتزاع INotificationAction برقرار شود. در نتیجه داریم:

class EventLogWriter : INofificationAction
{   
    public void ActOnNotification(string message)
    {
        // نوشتن گزارشات در رویدادها
    }
}

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

class EmailSender : INofificationAction
{
    public void ActOnNotification(string message)
    {
        // انجام عملیات ارسال ایمیل
    }
}

class SMSSender : INofificationAction
{
    public void ActOnNotification(string message)
    {
        // انجام عملیات ارسال SMS
    }
}

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

تزریق وابستگی Dependency Injection

اما با تمام این وجود هنوز یک مشکل باقی‌ است. اگر به کدهای موجود در کلاس AppPoolWatcher نگاه کنید. هنگامی‌که شرط action==null برقرار بود چه اتفاقی باید بیفتد؟ باید یکی از عملیات‌های یادداشت خطاها یا ارسال ایمیل و SMS‌ توسط رابط‌ها یا انتزاع‌ها صورت بگیرد. درست است؟ و اگر این کار انجام شود شما با کد زیر مواجه خواهید شد:

class AppPoolWatcher
{
    // ساخت یک انتزاع یا رابط جدید
    INofificationAction action = null;

    // تابعی که گزارش خطاها را یادداشت می‌کند
    public void Notify(string message)
    {
        if (action == null)
        {
            // اعمال کلاس انتزاع یا Interface
            writer = new EventLogWriter();
        }
        action.ActOnNotification(message);
    }
}

اما با این نوشتار ما مجددا به خانه اول بازگشتیم. حال برای حل این مشکل از مفهوم Dependency Injection‌ یا تزریق وابستگی استفاده می‌شود.

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

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

تزریق وابستگی از طریق ۲ روش عمده صورت می‌پذیرد:

  1. تزریق سازنده (Constructor Injection)
  2. تزریق متد (Method Injection)
  3. تزریق ویژگی (Property Injection)

تزریق سازنده (Constructor Injection)

در این روش شی‌ای از کلاس سطح پایین به سازنده کلاس سطح بالا ارسال می‌شود که در نهایت نوع آن از جنس Interface است. بنابراین برای مثال فوق تغییرات زیر را خواهیم داشت:

class AppPoolWatcher
{
    // ایجاد یک EventLogWriter با استفاده از رابط
    INofificationAction action = null;

    public AppPoolWatcher(INofificationAction concreteImplementation)
    {
        this.action = concreteImplementation;
    }

    // تابعی که عملیات ذخیره سازی را انجام می‌دهد
    public void Notify(string message)
    {   
        action.ActOnNotification(message);
    }
}

در این مثال همانطور که مشاهده می‌کنید آرگومان تابع سازنده برابر است با یک شیء از کلاس سطح پایین که به واسطه‌ی رابط ایجاد شده است. بنابراین برای اینکه از کلاس EventLogWriter بتوانیم استفاده کنیم باید دستورهای زیر را به کار ببریم:

EventLogWriter writer = new EventLogWriter();
AppPoolWatcher watcher = new AppPoolWatcher(writer);
watcher.Notify("Sample message to log");

همینطور در جریان هستید که اگر بخواهید عملیاتی مانند ایمیل یا SMS را به کلاس سطح بالا ارسال کنید کافی‌ست آرگومان AppPoolWatcher را موقع فراخوانی تغییر دهید.

تزریق متد (Method Injection)

در تزریق سازنده مشاهده کردید که کلاس سطح بالا حتما و حتما از یک کلاس سطح پایین برای اجرای فرآیند پیش‌فرض خود استفاده کرد. حال اگر بخواهیم تنها یک متد مشخص از یک کلاس را درگیر بحث تزریق وابستگی کنیم باید به چه صورت عمل کرد؟ باید تنها و تنها آن وابستگی را به یک آرگومان یک متد مشخص ارسال کنیم. بنابراین تزریق متد تنها و تنها روی یک متد مشخص صورت میگیرد و طی آن وابستگی‌های کلاس‌های سطح پایین به متدی از کلاس سطح بالا ارسال می‌شود. بنابراین برای کلاس AppPoolWatcher داریم:

class AppPoolWatcher
{
    INofificationAction action = null;

    public void Notify(INofificationAction concreteAction, string message)
    {
        this.action = concreteAction;
        action.ActOnNotification(message);
    }
}

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

EventLogWriter writer = new EventLogWriter();
AppPoolWatcher watcher = new AppPoolWatcher();
watcher.Notify(writer, "Sample message to log");

IoC Container

با مطالعه‌ی مطالب فوق متوجه شدید که در حالت کلی از دو نوع تزریق وابستگی به سازنده‌ها و تزریق وابستگی به متدها استفاده می‌شود. اما گاها  این سوال پیش می‌آید که اگر این وابستگی‌ها به صورت تودرتو باشند چکار باید کرد؟ در این حالت از مفهومی تحت عنوان IoC Container استفاده می‌شود تا به اصطلاح نقشه‌کشی وابستگی‌ها به ساده‌ترین شکل ممکن صورت بگیرد و توسعه‌دهنده را دچار سردرگمی نکند.

مثالی دیگر

حال برای روشن‌تر شدن این مفاهیم مثال دیگری را مطرح خواهیم کرد تا به درک و فهم شما نسبت به بحث تزریق وابستگی کمک بیشتری کرده باشیم:

برای مثال دو کلاس Class1 و Class2 را در نظر بگیرید که به یکدیگر لینک شده اند. به مجموعه‌ی کد زیر توجه کنید:

public class Class1
{
    public Class2 Class2 { get; set; }
}
 
public class Class2
{
}

به تصویر زیر دقت کنید. دو کلاس Class1 و Class2 را برای شما شبیه‌سازی کرده‌ایم. که در آن هر دو کلاس به یکدیگر کاملا چفت شده‌اند.

رابطه مستقیم کلاس ها

معمولا اگر کلاس Class1 و کلاس Class2 به صورت استحکام ضعیف (Loosely Coupled) باشند، باید Class1 به صورت غیرمستقیم به کلاس ۲ وابسته باشد. به کدهای زیر توجه کنید:

public class Class1
{
    public IClass2 Class2 { get; set; }
}
 
public interface IClass2 
{
}
 
public class Class2 : IClass2
{
 
}

حال همانطور که در تصویر زیر هم مشاهده می‌کنید کلاس ۱ بدون وابستگی مستقیم به کلاس ۲ اجرا می‌شود چون به جای ارتباط مستقیم از ارتباط با واسطه استفاده شده است:

رابطه کلاس سطح بالا و سطح پایین

اما این نوع نوشتار همچنان وابستگی را حذف نکرده است. بلکه یک ارتباط مستقیم را به یک ارتباط غیرمستقیم با واسطه تبدیل کرده. بنابراین اگر Class1 بخواهید یک نمونه جدید (new instance) بسازد باید همواره کلاس ۲ را نیز اجرا کند. چون هنوز به کلاس ۲ وابسته است. به کد زیر توجه کنید:

public class Class1
{
    public Class1()
    {
        Class2 = new Class2();           
    }
    public IClass2 Class2 { get; set; }
}
 
public interface IClass2 
{
}
 
public class Class2 : IClass2
{
}

همانطور که مشاهده می‌کند بین سازنده‌ی پیشفرض Class1 و ایجاد نمونه‌ی جدید کلاس Class2 هنوز استحام محکم (tight coupled) وجود دارد. در نتیجه برای حذف این وابستگی از DI یا تزریق وابستگی استفاده می‌کنیم. به کد زیر دقت کنید:

public class Class1
{
    public readonly IClass2 _class2;
 
    public Class1():this(DependencyFactory.Resolve<IClass2>())
    {
 
    }
 
    public Class1(IClass2 class2)
    {
        _class2 = class2;
    }
}

یعنی کلاس نمونه‌سازی کلاس دوم را به‌گونه‌ای انجام داده‌ایم که وابستگی کلاس ۱ به آن حذف شود.

امیدوارم این آموزش به عنوان یک مرجع جامع فارسی در زمینه توضیح تزریق وابستگی (Dependency Injection) مورد پسند شما عزیزان قرار گرفته باشد. چنانچه سوال و یا نظری دارید می‌توانید از طریق انجمن و یا بخش نظرات اعلام کنید.

منتشر شده در برنامه نویسی