آموزش اصل معکوس سازی کنترل (Inversion of Control) در برنامه نویسی

پرووید

Inversion of Control

در این پست از وبسایت پرووید در رابطه با آموزش اصل معکوس سازی کنترل (Inversion of Control) در برنامه نویسی صحبت خواهیم کرد.

شروع کار با Inversion of Control

در این قسمت ما در رابطه با Inversion of Control و نحوه ی پیاده سازی آن صحبت خواهیم کرد. این اولین قدم در رسیدن به یک طراحی Loosely Coupled است که می توانید جزئیات بیشتر آن را در تصویر زیر ببینید.

اصل Inversion of Control یک اصل طراحی (Design Principle) است هر چند که ممکن است بعضی از افراد از آن به عنوان یک الگوی طراحی یاد کنند. همانطور که نام Inversion of Control بر می آید از این اصل برای معکوس کردن انواع مختلفی از کنترل ها در یک طراحی شی گرا استفاده می شود. در این تعریف معنی کلمه کنترل هر مسئولیت مازادی نسبت به مسئولیت اصلی یک کلاس است. برای مثال کنترل جریان (Flow) برنامه و یا کنترل بر روی اشیای که یک کلاس به آن وابسته هستند از قبیل کنترل ساخت یک شی از ان کلاس یا Bind کردن آن. این معکوس سازی ما را به سمت یک طراحی Loose Coupled سوق می دهد.

بیایید یک مثال ساده تر از اصل Inversion of Control را در دنیای واقعی با هم بررسی کنیم. فرض کنید که شما برای رفتن به س رکار ماشینی را دارید که مجبور هستید آن را برای چند کیلومتر به سمت دفتر کار خود برانید. در چنین شرایطی کنترل ماشین در دست شماست یعنی شما ماشین را کنترل می‌کنید و یا شما رانندگی را به عهده میگیرید. بر اساس اصل Inversion of Control اگر بخواهید این کنترل را معکوس کنید به این معنی است که یک راننده استخدام کنید تا وی ماشین را برای شما براند و شما را به سر کار برساند. در واقع با انجام چنین کاری مسئولیت کنترل کردن ماشین و یا همان رانندگی از شما گرفته شده و به شخصی دیگر داده می‌شود. در این حال شما می‌ توانید در ماشین نشسته و تمرکزتان را بر روی کاری دیگر بگذارید و راننده‌ ای که استخدام کرده اید مسئولیت رانندگی را به عهده بگیرد. اصل Inversion of Control مزیت ‌های زیادی را دارد. برای مثال رسیدن به یک طراحی Loose Coupled و یا بهبود قابلیت های قابل تست بودن یا همان Testability و نگهداری یا همان Maintainability و قابل گسترش بودن یا همان Extensibility. در ادامه در رابطه با اصل Inversion of Control و انواع مختلفی از کنترل که می توانند معکوس شوند صحبت خواهیم کرد.

کنترل Flow (جریان) برنامه

در یک برنامه ساده Console Application در سی شارپ اجرای برنامه در متد Main این اتفاق می ‌افتد. در واقع می‌توان گفت که متد Main کنترل Flow برنامه یا تعامل هایی که برنامه با کاربر برقرار خواهد کرد را به عهده دارد. به کد زیر که مثالی از یک برنامه ساده Console Application است دقت کنید.

namespace FlowControlDemo
{
    class Program
    {
        static void Main(string[] args)
        {
           bool continueExecution = true;
            do
            {
                Console.Write("Enter First Name:");
                var firstName = Console.ReadLine();

                Console.Write("Enter Last Name:");
                var lastName = Console.ReadLine();

                Console.Write("Do you want to save it? Y/N: ");

                var wantToSave = Console.ReadLine();

                if (wantToSave.ToUpper() == "Y")
                    SaveToDB(firstName, lastName);

                Console.Write("Do you want to exit? Y/N: ");

                var wantToExit = Console.ReadLine();

                if (wantToExit.ToUpper() == "Y")
                    continueExecution = false;

            }while (continueExecution);
         
        }

        private static void SaveToDB(string firstName, string lastName)
        {
            //save firstName and lastName to the database here..
        }
    }
}

در کد بالا متد Main در کلاس Program وظیفه کنترل کردن Flow برنامه را دارد. به عبارت دیگر ورودی‌ هایی را از کاربر گرفته و آنها را ذخیره میکند. و سپی کار خود را با خارج شدن از برنامه و یا ادامه دادن آن بر اساس ورودی که از سمت کاربر وارد می‌شود ادامه می‌دهد. در واقع Flow برنامه توسط متد Main کنترل می شود. اگر بخواهیم اصل Inversion of Control را بر روی این برنامه اعمال کنیم باید کنترل Flow برنامه را از متد Main گرفته و آن را به کاربر بدهیم. شاید باورتان نشود اما به سادگی می‌توان با تبدیل کردن این برنامه به یک برنامه Windows Forms Application اصل Inversion of Control را در آن پیاده کرد.

با استفاده از این روش کنترل Flow برنامه از متد Main گرفته شده و به کاربر داده می‌شود و کاربر می‌تواند از طریق واسط کاربری و Event هایی که توسط وی Raise می شوند برنامه را پیش ببرد.

کنترل ساختن وابستگی‌ ها

اصل Inversion of Control می تواند به روشی دیگر برای کنترل کردن وظیفه ساختن اشیایی که به آنها وابستگی داریم نیز استفاده شود. در ابتدا بیایید در قالب یک مثال ساده ببینیم منظور از وابستگی چیست. لطفاً کدی که در ادامه آمده است را نگاه کنید.

public class A
{
    B b;

    public A()
    {
        b = new B();
    }

    public void Task1() {
        // do something here..
        b.SomeMethod();
        // do something here..
    }

}

public class B {

    public void SomeMethod() { 
        //doing something..
    }
}

در کدی که در قسمت بالا مشاهده کردید کلاس A متد SomeMethod از کلاس B را برای انجام دادن وظیفه خود که تحت عنوان Task1 تعریف شده است صدا می ‌زند. در واقع کلاس A نمی تواند این وظیفه را بدون استفاده از کلاس B و متد مذکور انجام دهد. به همین دلیل می توانیم بگوییم که کلاس A وابسته به کلاس B است یا اینکه کلاس B یک وابستگی یا همان Dependency برای کلاس A است. در طراحی شی گرا کلاس ها اغلب نیاز به تعامل با همدیگر دارند تا بتوانند وظایف برنامه یا Functionality های آن را انجام دهند.

همانطور که در مثال بالا دیدید کلاس A برای انجام کاری به یک شی از کلاس B نیازمند است. در مثال بالا کلاس A یک شی از کلاس B را می‌سازد و حتی چرخه حیات (Lifetime) آن را نیز مدیریت می‌کند. ساده بگوییم کنترل ساخت و چرخه حیات کلاس B به عهده ی کلاس A است. بنابراین یک وابستگی بین کلاس A و کلاس B وجود دارد. بر اساس اصل Inversion of Control ما باید این کنترل را معکوس کنیم. به عبارتی وظیفه ساختن یک شی از کلاس B را نه به کلاس A بلکه به یک کلاس دیگر بدهیم. همانطور که در کد زیر مشاهده می کنید وظیفه ساختن این وابستگی که همان کلاس B است را از کلاس A گرفته و آن را به یک کلاس دیگر تحت عنوان Factory داده ایم.

public class A
{
    B b;

    public A()
    {
        b = Factory.GetObjectOfB ();
    }

    public void Task1() {
        // do something here..
        b.SomeMethod();
        // do something here..
    }
}

public class Factory
{
    public static B GetObjectOfB() 
    {
        return new B();
    }
}

با این تغییر دیگر کلاس A مسئول ساختن یک شی از کلاس B نیست بلکه کلاس A از یک کلاس دیگر به نام Factory برای به دست آوردن یک شی از کلاس B استفاده می کنند.

حال بیایید همین موضوع یعنی کنترل ساختن Dependency ها را در یک مثال کاربردی تر ببینیم. در طراحی شی گرا کلاس ها باید به صورت از Loosely Coupled طراحی شوند. Loosely Coupled بودن به این معنی است که تغییرات در یک کلاس نباید باعث ایجاد تغییراتی در کلاس ‌های دیگر شود. این باعث می شود که برنامه به طور کلی قابلیت نگهداری (Maintainability) و قابلیت گسترش (Extensibility) بالاتری داشته باشد. بیایید با نگاه کردن به تصویر زیر که یک معماری N-Tier معمول را نشان می دهد این موضوع را بررسی کنیم.

در یک معماری N-Tier معمول واسط کاربری برنامه (UI) از لایه سرویس (Service Layer) برای بازیابی و ذخیره کردن داده ها استفاده می‌کند. لایه ی سرویس (Service Layer) از کلاس‌ Business Logic برای اعمال کردن Business Rule ها بر روی داده استفاده می‌کند. کلاس Business Logic به کلاس Data Acces وابستگی دارد و وظیفه ی کلاس Data Access بازیابی یا ذخیره کردن داده ها بر روی دیتابیس است. این یک معماری N-Tier ساده است. در ادامه بیاید بر روی کلاس های Data Access و Business Logic تمرکز کرده تا بتوانیم مفهوم Inversion of Control را در این مثال متوجه شویم.

کدی که در ادامه می بینید دو کلاس ساده ی Business Logic و Data Access را نشان می دهد که برای Entity ی به نام Customer یا همان مشتری پیاده‌ سازی شده اند.

public class CustomerBusinessLogic
{
    DataAccess _dataAccess;

    public CustomerBusinessLogic()
    {
        _dataAccess = new DataAccess();
    }

    public string GetCustomerName(int id)
    {
        return _dataAccess.GetCustomerName(id);
    }
}

public class DataAccess
{
    public DataAccess()
    {
    }

    public string GetCustomerName(int id) {
        return "Dummy Customer Name"; // get it from DB in real app
    }
}

همانطور که از کد بالا متوجه می‌شوید کلاس CustomerBusinessLogic به کلاس DataAcces وابستگی دارد. به عبارت دیگر این کلاس برای خواندن اطلاعات مربوط به مشتری یک شی از کلاس DataAccess را در درون خود ایجاد می‌کند. حال بیایید در رابطه با مشکلات این نوع طراحی صحبت کنیم.

در مثال بالا دو کلاس CustomerBusinessLogic و DataAccess با هم Tight Coupling (در هم تنیدگی سخت) دارند چرا که CustomerBusinessLogic شامل یک رفرنس به کلاس DataAccess است. به عبارت دیگر یک شی از کلاس DataAccess در درون CustomerBusinessLogic تعریف شده و چرخه حیات (Lifetime) آن توسط این کلاس مدیریت می شود.

مشکلات مربوط به این نوع طراحی از قرار زیر می باشند:

  • کلاسهای CustomerBusinessLogic و DataAccess با هم Tight Coupling دارند. بنابراین تغییرات در کلاس DataAccess منجر به تغییرات در کلاس CustomerBusinessLogic می شود. برای مثال اگر ما یک متد در DataAccess را حذف و یا اضافه کنیم یا حتی تغییر نام دهیم نیازمند تغییر دادن کد های CustomerBusinessLogic که از این متد استفاده می کنند نیز هستیم.
  • فرض کنید که در آینده امکان دارد داده های مربوط به CustomerBusinessLogic از دیتابیس ها یا وب سرویس های مختلفی وارد برنامه شوند. برای مدیریت کردن چنین شرایطی نیاز به ساخت کلاس های DataAccess دیگر داریم تا بتوانیم داده ها را از منابع مختلف دریافت کنیم. این تغییرات نیز منجر به ایجاد تغییراتی در CustomerBusinessLogic نیز می شوند.
  • همانطور که می دانید کلاس CustomerBusinessLogic یک شی از کلاس DataAccess را با استفاده از کلمه کلیدی new در درون خود ایجاد می‌کند. ممکن است کلاس های متفاوتی در برنامه ما وجود داشته باشند که از کلاس DataAccess استفاده کنند و از آن یک شی بسازند. بنابراین اگر نام کلاس DataAccess در آینده تغییر کند باید تمامی مکان هایی که یک شی از این کلاس ساخته شده است را پیدا کرده و این تغییرات را در آنجا اعمال کنیم.
  • از آنجایی که کلاس CustomerBusinessLogic یک شی از کلاس DataAccess (دقت کنید که DataAccess یک کلاس Concrete است نه یک کلاس Abstract یا اینترفیس) را در درون خود ایجاد کرده است نمی‌تواند به طور مستقل در Test Driven Development استفاده شود. به عبارت دیگر CustomerBusinessLogic نمی تواند با کلاس‌های Mock جایگزین شود. اگر در این رابطه اطلاعات بیشتری می خواهید توصیه می‌کنیم حتماً از بسته های آموزش وب سایت پرووید در رابطه با Test Driven Development و Unit Testing استفاده کنید.

رای حل تمامی مشکلاتی که از آنها یاد شد و رسیدن به یک طراحی Loosely Coupled ما می‌توانیم از اصول Inversion of Control و Dependency Inversion استفاده کنیم. دقت کنید که Inversion of Control یک Principle است و نه یک Design Pattern. این که Inversion of Control یک Principle است به این معنی است که توصیه های سطح بالایی (High Level Guideline) در رابطه با طراحی را ارائه می ‌کند و ربطی به جزئیات پیاده‌ سازی (Implemention Detail) ندارد. شما می توانید اصل Inversion of Control را به روش های مختلفی در برنامه خود پیاده سازی کنید.

تصویری که در ادامه می بینید یکی از روش ‌های پیاده‌سازی اصل Inversion of Control است.

بیاید از الگوی طراحی Factory برای پیاده ‌سازی اصل Inversion of Control استفاده کنیم. این موضوع اتفاقی است که در تصویر بالا نیز نشان داده شده است. این اولین قدم ما در رسیدن به کلاس‌ های Loosely Coupled می باشد. برای انجام این کار ابتدا یک کلاس ساده با نام DataAccessFactory را ایجاد می کنیم. همانطور که در تصویر زیر می بینید در درون این کلاس متد GetDataAccessObj یک شی از کلاس DataAccess را بر میگرداند.

public class DataAccessFactory
{
    public static DataAccess GetDataAccessObj() 
    {
        return new DataAccess();
    }
}

سپس در درون کلاس CustomerBusinessLogic از این کلاس استفاده کرده و توسط آن یک شی از کلاس DataAccess را به دست می آوریم. کدی که در ادامه می بینید این موضوع را نشان می دهد.

public class CustomerBusinessLogic
{

    public CustomerBusinessLogic()
    {
    }

    public string GetCustomerName(int id)
    {
        DataAccess _dataAccess =  DataAccessFactory.GetDataAccessObj();

        return _dataAccess.GetCustomerName(id);
    }
}

همانطور که در کد بالا مشهود است کلاس CustomerBusinessLogic از متد GetDataAccessObj از کلاس DataAccessFactory برای ساختن یک شی جدید از کلاس DataAccess استفاده می شود. این روش را مقایسه کنید با روش قبلی که از کلید واژه new برای ساخت یک شی از کلاس DataAccess در درون کلاس CustomerBusinessLogic استفاده می شد. به طور خلاصه ما مسئولیت ساختن و کنترل کردن یک شی جدید از کلاس DataAccess که Dependency کلاس CustomerBusinessLogic است را از آن گرفته و به کلاس DataAccessFactory تحویل داده ایم.

این روش ساده ترین پیاده سازی اصل Inversion of Control و اولین قدم ما در رسیدن به یک طراحی کاملا Loose Coupled است. اما همانطور که در قسمت های قبلی این آموزش ذکر شد صرفا با استفاده از اصل Inversion of Control نمی توان به یک طراحی کاملا Loose Couples رسید. در کنار اصل Inversion of Control ما نیاز به استفاده کردن از اصل Dependency Inversion الگوی طراحی Strategy و Dependency Injection داریم.