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

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

در این پست از وبسایت پرووید در رابطه با آموزش تکنیک تزریق وابستگی (Dependency Injection) در برنامه نویسی شی گرا صحبت خواهیم کرد.

در این قسمت از آموزش وب سایت پرووید قصد داریم در رابطه با Dependency Injection در برنامه نویسی و طراحی شی گرا صحبت کنیم. در واقع Dependency Injection یک تکنیک است که با استفاده از آن می توانیم Loose Coupling را به دست آوریم چرا که با استفاده از این تکنیک Dependency Injection های یک Object به آن Object تحویل داده می‌شوند و در واقع خود آن Object مسئولیت ساختن و یا اصطلاحاً Instantiate کردن Dependency های خود را عهده دار خواهد شد.

وابستگی (Dependency) در طراحی شی گرا چیست؟

به منظور فهمیدن مفهوم Dependency Injection و یا تزریق وابستگی ابتدا باید مفهوم Dependency را درک کنیم. در طراحی شی گرا ممکن است Component های مختلف اغلب به دیگر Component ها وابسته باشند. این وابستگی را Dependency می گوئیم. دقت کنید که منظور از Component در این قسمت اجزای تشکیل دهنده یک طراحی شی گرا می باشد. برای مثال یک کلاس می تواند نقش یک Component را ایفا کند. پس در یک طراحی شی گرا ممکن است کلاس ها به یکدیگر وابستگی داشته باشند. در چنین شرایطی ساده ترین کاری که به نظر می رسد این است که از درون کلاسی که به کلاس دوم وابستگی دارد اقدام به ایجاد کردن یک Object از کلاس دوم کنیم. این موضوع با استفاده از کلمه کلیدی New در سی شارپ اتفاق می افتد.

بررسی یک مثال کاربردی از وجود وابستگی (Dependency)

بیاید یک مثال کاربردی را باهم بررسی کنیم. کدی که در قسمت زیر قرار داده ایم را ببنید.

public class PaymentTerms
{
    PaymentCalculator _calculator = new PaymentCalculator();
 
    public decimal Price { get; set; }
    public decimal Deposit { get; set; }
    public int Years { get; set; }
 
    public decimal GetMonthlyPayment()
    {
        return _calculator.GetMonthlyPayment(Price, Deposit, Years);
    }
}
 
 
public class PaymentCalculator
{
    public decimal GetMonthlyPayment(decimal Price, decimal Deposit, int Years)
    {
        decimal total = Price * (1 + Years * 0.1M);
        decimal monthly = (total - Deposit) / (Years * 12);
        return Math.Round(monthly, 2, MidpointRounding.AwayFromZero);
    }
}

در این کد یک کلاس با نام PyamentTerms تعریف شده است، که در بردارنده جزئیات مورد نیاز برای محاسبه کردن اقساط یک وام بانکی است. در درون این کلاس سه Property وجود دارند و همچنین یک Method با نام GetMonthlyPayment مسئول محاسبه کردن اقساط ماهیانه با استفاده از یک Object از کلاس PaymentCalculator است. پس می توانیم ببینم که کلاس PaymentTerms یک Dependency به کلاس PaymentCalculator دارد و یا حتی می توان گفت که کلاس PaymentCalculator یک Dependency برای کلاس PaymentTerms است. به عبارت ساده یک Dependency یک Object است که یک Object دیگر برای انجام کارش به آن وابسته است. در مثالی که در این قسمت در حال بررسی کردن آن هستیم، Dependency مورد نظر که همان کلاس PaymentTerms است، در زمان ساخته شدن یک Object از کلاس PaymentTerms ساخته می شود.

بسته ی آموزش ویدئویی Inversion of Control و IoC Container ها

از شما دعوت می کنیم که از بسته ی آموزش ویدئویی Inversion of Control و IoC Container ها دیدن کنید.

بررسی مشکلات طراحی شی گرا بدون تزریق وابستگی

یکی از مشکلات بسیار خطرناک در رابطه با کد بالا ساخته شدن یک Object از کلاس PaymentCalculator در درون کلاس PaymentTerms می باشد. از آنجایی که این Dependency به طور مستقیم در کلاس PaymentTerms ساخته شده است، این دو کلاس اصطلاحاً به یکدیگر Tightly Coupled خواهند شد. اگر در آینده براساس نیازمندیهای جدید نیاز به استفاده کردن از یک PaymentCalculator متفاوت داشته باشیم، نیاز است که برای انجام این کار کلاس PaymentTerms نیز دستخوش تغییر شود که این خود با اصول SOLID در طراحی شی گرا در تناقض است.

به طور مشابه اگر بخواهیم با استفاده از عملیات Automated Testing اقدام به تست کردن کلاس PaymentTerms کنیم، این کار با Tight Coupling موجود بین این دو کلاس عملاً غیر قابل انجام خواهد بود. حال مکانیزم Dependency Injection سعی در کاهش دادن این Tight Coupling خواهد کرد. این موضوع با حذف کردن وظیفه ساختن Dependency های یک کلاس توسط خود آن کلاس انجام خواهد شد. به عبارت دیگر به جای اینکه یک کلاس خود مسئول ساختن Dependency های خود بشود، این Dependency ها توسط یک Component دیگر ساخته شده و در زمان مورد نیاز به آن کلاس وابسته تحویل خواهند شد. این آموزش از وب سایت پرووید در رابطه با سه روش مختلف برای پیاده سازی Dependency Injection صحبت خواهیم کرد. این روش ها را Interface Injection و Setter Injection و Constructor Injection می نامند.

بررسی روش Interface Injection

در روش Interface Injection اتفاقی که می‌افتد این است که Dependency های یک کلاس وابسته با استفاده از یک Method به آن تحویل داده خواهند شد. این Method در درون یک Interface تعریف شده است و توسط کلاس وابسته پیاده سازی می شود. در این روش Dependency ها به عنوان پارامترهای این Method قبل از اینکه Dependency مورد نیاز باشد، به آن تحویل داده خواهد شد. لطفاً تصویر زیر را مشاهده کنید.

تصویر بالا یک Class Diagram در UML را نشان می دهد، که روش پیاده سازی Interface Injection را به تصویر کشیده است. در ادامه به بررسی هر کدام از این کلاس ها و اینترفیس ها خواهیم پرداخت.

اینترفیس IDependent: در درون این Interface متدی که مسئول تزریق کردن و یا Inject کردن Dependency ها می باشد را تعریف می کنیم.

کلاس Dependent: این کلاس، کلاسی است که به یک کلاس دیگر وابستگی دارد. در زبان انگلیسی کلمه Dependent به معنی وابسته است. در این روش کلاس وابسته، اینترفیس IDependent را پیاده سازی می کند تا اجازه دهد که Dependency های مورد نظرش با استفاده از متدهای تعریف شده در این اینترفیس به درون آن ترزیق بشوند.

در مثالی که در قسمت بالا مشاهده کردید، این Dependency در درون یک فیلد Private تعریف شده است.

اینترفیس IDependency: این اینترفیس دارای Member هایی است که توسط تمامی Dependency های مورد نظر پیاده سازی می شوند. با استفاده از این اینترفیس اتفاقی که می افتد این است که Dependency ها می توانند به راحتی توسط Type های دیگر جایگزین بشوند و این موضوع در انجام Automated Testing و استفاده کردن از Fake ها و Stop ها و Mock ها کاربرد دارند.

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

کد مربوط به این Class Diagram را در قسمت زیر مشاهده می کنید.

public interface IDependency
{
    void SomeMethod();
}
 
 
public class Dependency : IDependency
{
    public void SomeMethod()
    {
        Console.WriteLine("Dependency.SomeMethod() called");
    }
}
 
 
public interface IDependent
{
    void InjectDependency(IDependency dependency);
}
 
 
public class Dependent : IDependent
{
    IDependency _dependency;
 
    public void InjectDependency(IDependency dependency)
    {
        _dependency = dependency;
    }
 
    public void CallDependency()
    {
        _dependency.SomeMethod();
    }
}

پیاده سازی روش Interface Injection

در قسمت قبل در رابطه با دو کلاس با نام های PaymentTerms و همچنین PaymentCalculator صحبت کردیم. مشاهده کردید که بین این دو کلاس یک در هم تنیدگی تنگاتنگ و یا اصطلاحاً Tight Coupling وجود دارد. به عبارت دیگر یک Object از کلاس PaymentCalculator به عنوان Dependency به طور مستقیم در کلاس PaymentTerms ساخته می شد. در اینجا قصد داریم که با پیاده سازی روش Interface Injection این دو کلاس را از هم Decouple کرده و Tight Coupling بین آن ها را از بین ببریم. اولین کار تعریف کردن یک اینترفیس برای PaymentCalculator می باشد. نام این اینترفیس را IPaymentCalculator می گذاریم. تعریف این اینترفیس در قسمت زیر نشان داده شده است.

public interface IPaymentCalculator
{
    decimal GetMonthlyPayment(decimal Price, decimal Deposit, int Years);
}

پس از این، کلاس PaymentCalculator باید اینترفیس IPaymentCalculator را پیاده سازی کند. این موضوع در کد زیر نشان داده شده است.

public class PaymentCalculator : IPaymentCalculator

قدم بعدی تعریف کردن یک اینترفیس به منظور هندل کردن عملیات تزریق و یا Injection می باشد. از انجایی که برای کلاس PaymentTerms در حال حاضر فقط یک Dependency موجود است، اینترفیس مورد نظر فقط دارای یک متد است که یک پارامتر از نوع IPaymentCalculator را به عنوان ورودی دریافت می کنند. این موضوع در کد زیر نشان داده شده است.

public interface IPaymentCalculatorInjecter
{
    void InjectCalculator(IPaymentCalculator calculator);
}

قدم آخر تغییری است که در کلاس PaymentTerms رخ خواهد داد. این کلاس باید طوری تغییر کند که اینترفیس IPaymentCalculatorInjector را پیاده سازی کند. کدی که در قسمت زیر مشاهده می کنید این موضوع را نشان می دهد.

public class PaymentTerms : IPaymentCalculatorInjecter
{
    IPaymentCalculator _calculator;
 
    public void InjectCalculator(IPaymentCalculator calculator)
    {
        _calculator = calculator;
    }
 
    public decimal Price { get; set; }
    public decimal Deposit { get; set; }
    public int Years { get; set; }
 
    public decimal GetMonthlyPayment()
    {
        return _calculator.GetMonthlyPayment(Price, Deposit, Years);
    }
}

علاوه بر این دقت کنید که متد Injection یا متد InjectCalculator که مربوط به اینترفیس IPaymentCalculatorInjector است، اقدام به ذخیره کردن Dependency مورد نظر در درون یک فیلد Private می کند.

بسته ی آموزش ویدئویی اصول SOLID در طراحی شی گرا

از شما دعوت می کنیم که از بسته ی آموزش ویدئویی اصول SOLID در طراحی شی گرا دیدن کنید.

تست کردن روش Interface Injection

در ادامه می توانیم مثال ریفکتور شده توسط روش Interface Injection را با اجرا کردن کدی که در قسمت زیر مشاهده می کنید، مورد تست قرار بدهیم. در این مثال یک Object از کلاس PaymentTerms ساخته شده است و سپس یک Object از کلاس PaymentCalculator به درون آن Inject شده است. پس از آن Property های مربوط به PaymentTerms مقدار گرفتند و در نهایت خروجی تولید شده در Console چاپ شده است.

PaymentTerms pt = new PaymentTerms();
pt.InjectCalculator(new PaymentCalculator());
pt.Price = 10000;
pt.Years = 4;
pt.Deposit = 2500;
Console.WriteLine(pt.GetMonthlyPayment());  // Outputs "239.58"

پیاده سازی روش Setter Injection

روش دیگر پیاده سازی Dependency Injection استفاده کردن از روش Setter Injection است که در آن می توانیم Dependency ها را در قالب Property هایی که در درون کلاس وابسته تعریف می شوند، فراهم کنیم. این روش نیز شبیه Interface Injection عمل می کند، چرا که در این روش یک Member برای Inject کردن یک Dependency به درون کلاس وابسته اجرا خواهد شد. در بعضی موارد ممکن است که Property مورد نظر را در درون یک اینترفیس تعریف کنیم. هرچند که این موضوع عملاً ضرورتی ندارد. لطفا تصویر زیر را مشاهده کنید.

در Class Diagram ی که در تصویر بالا مشاهده می کنید، پیاده سازی روش Setter Injection به تصویر کشیده شده است. اینترفیس ها و کلاس های این روش عبارتند از :

کلاس Dependent: این کلاس نمایانگر کلاسی است که به یک Dependency نیاز دارند. در این روش Dependency مورد نظر با استفاده از یک Property با نام Dependency به درون کلاس Dependent تزریق می شود. علاوه بر این در تصویر بالا مشاهده می کنید که Dependency مورد نظر در درون یک فیلد Private ذخیره شده است.

اینترفیس IDependency: این اینترفیس نمایانگر تمامی Member هایی است که توسط تمامی Dependency ها پیاده سازی می شوند.

کلاس Dependency: این کلاس نمایانگر یک Dependency است که می تواند به درون کلاس وابسته و یا اصطلاحاً Dependent تزریق بشود.

کد مربوط به این Class Diagram در کد زیر نشان داده شده است.

public interface IDependency
{
    void SomeMethod();
}
 
 
public class Dependency : IDependency
{
    public void SomeMethod()
    {
        Console.WriteLine("Dependency.SomeMethod() called");
    }
}
 
 
public class Dependent
{
    IDependency _dependency;
 
    public IDependency Dependency
    {
        set { _dependency = value; }
    }
 
    public void CallDependency()
    {
        _dependency.SomeMethod();
    }
}

در این قسمت قصد داریم که مثال قبلی را طوری تغییر بدهیم که از روش Setter Injection استفاده کند. به منظور انجام این کار در ابتدا اینترفیس IPaymentCalculatorInjector را حذف کرده و کلاس PaymentTerms را تغییر خواهیم داد. در درون کلاس PaymentTerms در حال حاضر یک Property وجود دارد که به صورت فقط نوشتنی و اصطلاحاً Write-Only تعریف شده است. این پروپرتی Dependency مورد نظر را دریافت کرده و آن را در درون یک فیلد Private ذخیره می کند. کد زیر این موضوع را نشان می دهد.

public class PaymentTerms
{
    IPaymentCalculator _calculator;
 
    public IPaymentCalculator Calculator
    {
        set { _calculator = value; }
    }
 
    public decimal Price { get; set; }
    public decimal Deposit { get; set; }
    public int Years { get; set; }
 
    public decimal GetMonthlyPayment()
    {
        return _calculator.GetMonthlyPayment(Price, Deposit, Years);
    }
}

تست کردن روش Setter Injection

به منظور تست کردن روش Setter Injection می تواینم کد زیر را اجرا کنیم. این کد شبیه به کدی است که برای تست کردن روش Interface Injection نوشته ایم. تنها تفاوتی که در این قسمت وجود دارد، این است که در این مثال عملیات Injection توسط یک Property انجام می شود و نه یک Method.

PaymentTerms pt = new PaymentTerms();
pt.Calculator = new PaymentCalculator();
pt.Price = 10000;
pt.Years = 4;
pt.Deposit = 2500;
Console.WriteLine(pt.GetMonthlyPayment());  // Outputs "239.58"

پیاده سازی روش Constructor Injection

آخرین روشی که برای انجام عملیات Dependency Injection وجود دارد استفاده کردن از روش Constructor Injection است. همانطور که ممکن است بدانید منظور از Constructor همان تابع سازنده یک کلاس است. همانطور که از نام این روش مشخص است، Dependency مورد نظر به عنوان یک پارامتر ورودی از تابع سازنده کلاس وابسته تعریف می شود. با استفاده از این روش می توان مطمئن بود که Dependency های یک کلاس قبل از اینکه هر کدام از Member های دیگر آن کلاس اجرا بشوند به ان کلاس تحویل داده شده است.

علاوه بر این در این روش ممکن است که Dependency ها زمانی تحویل کلاس وابسته داده شوند که عملاً به آن ها نیازی ندارند. دقت کنید که در این روش در زمان ساخته شدن کلاس وابسته Dependency های آن به درون تابع سازنده کلاس وابسته تزریق می شوند. تصویر زیر Class Diagram مربوط به این روش را نشان می دهد.

همانطور که در تصویر بالا مشاهده می کنید کلاس ها و Dependency های مختلفی وجود دارند که به شرح آن ها می پردازیم.

کلاس Dependent: این کلاس نمایانگر یک کلاس Dependent و یا وابسته است. در درون این کلاس یک Dependency وجود دارد که با استفاده از پارامتر ورودی تابع سازنده به درون آن تزریق شده و سپس در درون یک فیلد Private قرار گرفته است.

اینترفیس IDependency: این اینترفیس وظیفه تعریف کردن Member هایی را به عهده دارد که توسط تمامی Dependency ها باید پیاده سازی بشود.

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

کد مربوط به این Class Diagram را در قسمت زیر مشاهده می کنید.


public interface IDependency
{
void SomeMethod();
}

public class Dependency : IDependency
{
public void SomeMethod()
{
Console.WriteLine("Dependency.SomeMethod() called");
}
}

public class Dependent
{
IDependency _dependency;

public Dependent(IDependency dependency)
{
_dependency = dependency;
}

public void CallDependency()
{
_dependency.SomeMethod();
}
}

در این قسمت قصد داریم که کد مربوط به مثالهای قبل را طوری تغییر بدهیم که از روش Constructor Injection استفاده کند. کلاس PaymentTerms را شبیه به کد زیر تغییر بدهید و دیگر کلاس ها را تغییر ندهید.

public class PaymentTerms
{
    IPaymentCalculator _calculator;
 
    public PaymentTerms(IPaymentCalculator calculator)
    {
        _calculator = calculator;
    }
 
    public decimal Price { get; set; }
    public decimal Deposit { get; set; }
    public int Years { get; set; }
 
    public decimal GetMonthlyPayment()
    {
        return _calculator.GetMonthlyPayment(Price, Deposit, Years);
    }
}

تست کردن روش Constructor Injection

در نهایت به منظور تست کردن روش Constructor Injection می توانیم شبیه به مثالهای قبل این کار را انجام بدهیم. در این مثال PaymentCalculator به عنوان پارامتر ورودی تابع سازنده کلاس PaymentTerms به آن داده شده است.

PaymentTerms pt = new PaymentTerms(new PaymentCalculator());
pt.Price = 10000;
pt.Years = 4;
pt.Deposit = 2500;
Console.WriteLine(pt.GetMonthlyPayment());  // Outputs "239.58"

امیدواریم که این آموزش از وب سایت پرووید نیز مورد توجه شما دوستان عزیز قرار گرفته باشد. در نهایت توصیه میکنیم حتما از آموزش اصل Inversion of Control در برنامه نویسی و همچنین آموزش اصول SOLID در برنامه نویسی شی گرا و به طور خاص بررسی اصل Dependency Inversion Principle در برنامه نویسی شی گرا که اخرین اصل از سری اصول پنجگانه اصول SOLID می باشند، دیدن کنید.

مرتضی گیتی
بدون نظر

ارسال نظر

نظر
نام
ایمیل
وب سایت