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

Dependency Injection

21 اسفند 1395
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) مورد پسند شما عزیزان قرار گرفته باشد. چنانچه سوال و یا نظری دارید می‌توانید از طریق انجمن و یا بخش نظرات اعلام کنید.

نویسنده شوید

دیدگاه‌های شما (23 دیدگاه)

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

مجید
14 اسفند 1398
درواقع service container یک آبجکت هست که در هر جای برنامه که به آن نیاز داشته باشید , قابل دسترسی می باشد. service container شامل یک سری زوج های key/value میباشد که کلید نام سرویس و مقدار , آبجکتی می باشد که سرویس را پیاده سازی می کند.شما می تونید هر کلید و مقداری را در آن قرار دهید.چیزی که باعث میشه service container فریم ورک لاراول رو قدرتمند کنه اینه که شما میتونید به service container بگید هر وقت کلیدی صدا زده شد چه چیزی اجرا بشه.

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

محمد علی امینی
14 مهر 1398
به نظرم خوب بود. فقط نحو زبان در مثال دوم را متوجه نشدم. این خط را می‌گویم: public Class2 Class2 { get; set; } این چه زبانی است؟

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

فهیمه
27 شهریور 1398
عالی توضیح دادین واقعا ممنون فقط مثال دوم ملموس نبود و یه سوال: اینکه بخواهیم از طریق کانستراکتور تزریق وابستگی داشته باشیم باز باید در کلاس سطح بالا به کلاس سطح پایین دسترسی داشته باشیم و اگر بخواهیم این اتفاق نیافته باید چه کنیم؟ ایا کاربرد ioc Container اینجا معنی میده؟

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

سیروس
10 شهریور 1398
عالی بود

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

C#
27 مرداد 1398
ممنون از آموزش خوبتون . ولی خدایی IoC Container و پیچوندینا .... :-D

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

ساسان
24 مرداد 1398
بهترین توضیحی بود که تاحالا دیدم. ممنون از شما. فقط مثال دوم رو کمی با عجله از روش رد شدید.

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

پارسا
16 تیر 1398
سلام. آموزش خوبی بود ممنون

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

علی اکبر رضائی
10 بهمن 1397
آموزش خوبی بود ممنون

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

امیر
26 آذر 1397
با سلام من توی پروژم از تزریق وابستگی از روش سازنده کلاس استفاده کردم. الان لازمم شده که یه تابعی از سرویس ها را توی فایل Startup.cs استفاده کنم ولی متاسفانه نمیدونم چجوری باید به این تابع دسترسی داشته باشم. به طور کلی چجور می توان سرویس ها را در فایل Startup.cs استفاده کرد. با تشکر

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

روکسو
26 آذر 1397
سلام وقت شما بخیر برای پرسش سوالات تخصصی خود مرتبط با برنامه نویسی لطفا به روکسو کیو مراجعه کنید.

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

مهدي
28 آبان 1397
IServiceProvider هم مربوط ميشه به اين؟

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

روکسو
29 آبان 1397
سلام وقت شما بخیر لطفا سوال خود را واضح تر بپرسید. در هر صورت شما می توانید از تزریق وابستگی برای استفاده کردن کلاس ها درون یکدیگر بهره ببرید.

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

فاطمه فلاحی
25 آبان 1397
سلام خیلی خوب بیان کردید، دست مریزاد

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

امیر
22 شهریور 1397
دمتون گرم .درکش برام سخت بود ولی شما خیلی خوب توضیح دادید .

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

مجتبی
19 شهریور 1397
اموزشی خوب و بسیار کاربردی بود خیلی ممنون

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

شیدا
21 مرداد 1397
خیلی عالی و منسجم بود واقعا مرسی فقط مثال اخرو درست متوجه نشدم

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

امیر
18 تیر 1397
خیلی ممنون بابت مطلب خوبتون ... فقط این بخش آخر که نوشتین IoC Container همون Property Injection هست ؟ آخه بالا سه نوع تزریق رو گفتین

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

مژگان
15 تیر 1397
خیلی ممنونم. عالی بود البته منم این بخش آخر رو متوجه نشدم: public class Class1 { public readonly IClass2 _class2; public Class1():this(DependencyFactory.Resolve()) { } public Class1(IClass2 class2) { _class2 = class2; } }

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

sami
06 خرداد 1397
تشکر فراوان :) واقعا توضیحات قابل فهم و مناسب بود.

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

میلاد
28 اردیبهشت 1397
ممنون... توضیح خوبی بود. ولی این آخری رو نفهمیدم

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

سمیرا
23 اسفند 1396
سلام نمیشد همینطوری بخونم و بدون تشکر برم واااقعا عالی بیان شده بود....چند روزه درگیر این مساله بودم...الان متوجهش شدم واقعا ممنونم

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

محمدرضا
07 آذر 1396
واقعا ساده و فشرده توضیح دادین فقط برای مثال دوم از نمونه‌های ملموس‌تر مثل animal و dog استفاده می‌کردین بهتر بود.

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

سبحان
24 مهر 1396
سلام خیلی زیبا بیان شده بود

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

مهدی
08 مهر 1396
خیلی خیلی خیلی ممنون از این مطلبتون، هر چی مقاله بودم در این مورد خوندم ولی اصلا نفهمیدم این مفاهیم چیه، ولی با این توضیحات شما کاملا فهمیدم چی به چیه

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

فردین سعادتی
09 مرداد 1396
مرسی از آموزش های عالیتون واقعا جا داره یه خسته نباشید و یه خدا قوت بهتون بگم خیلی خیلی ممنون بابت زحمات فراوان

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.