اصل معکوس سازی وابستگی‌ها (DIP)

در این مقاله قصد داریم در رابطه با یکی از اصول Solid ، که اصل معکوس سازی وابستگی‌ها (Dependency Inversion Principle) است ، بحث کنیم. و اصول کاری آن را مورد بررسی قرار دهیم.

 اصل معکوس سازی وابستگی‌ها (DIP)

1) مفاهیم :

DIP چیست ؟

حالت های اصلی :

1-  ماژول های سطح بالا نباید به ماژول های سطح پایین وابستگی داشته باشد. هر دوی آن ها باید  به abstraction ها وابستگی داشته باشند.

2- Abstraction ها نباید به جزئیات وابستگی داشته باشند. بلکه جزئیات باید به abstraction ها نیاز داشته باشند.

به عنوان مثال کد زیر به اصول فوق منطبق نیست :

public class HighLevelModule
{
  private readonly LowLevelModule _lowLowelModule;
 
  public HighLevelModule()
  {
    _lowLevelModule = new LowLevelModule();   
  }

  public void Call()
  {
    _lowLevelModule.Initiate();
    _lowLevelModule.Send();
  }
}

public class LowLevelModule
{
  public void Initiate()
  {
    //do initiation before sending
  }
  
  public void Send()
  {
    //perform sending operation
  }
}

در کد بالا ، ماژول های سطح بالا مستقیما به ماژول های سطح پایین وابستگی دارند. که این از اولین نکته DIP پیروی نمیکند.

رابطه مستقیم و محکم بین این دو باعث میشود که ساخت واحد تست روی ماژول های سطح بالا در ایزوله بودن از ماژول های سطح پایین سخت تر شود زیرا شما مجبور به تست  ماژول های سطح بالا  و  ماژول های سطح پایین به صورت همزمان با هم هستید.

 هنوز اجرای واحد تست روی ماژول های سطح بالا به صورت ایزوله با استفاده از فریمورک که از عملکرد NET CLR. جلوگیری میکند، ممکن است . از قبیل ایزوله کننده TypeMoc.

با استفاده از این فریمورک ، امکان تغییر رفتار ماژول های سطح پایین در تست میباشد. ما این کار را به دو دلیل توصیه نمیکنیم.

اولا استفاده از  CLR interception  میتواند در بدترین حالت نتایج مثبت کاذبی را به ما بدهد.

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

چگونه DIP ما اعمال میشود؟

اولین نکته از DIP به ما دو چیز را در زمان کد نشان میدهد :

• Abstraction

• وابستگی معکوس

در وهله اول ، ماژول های سطح پایین نیاز به abstract شدن دارند و ماژول های سطح بالا به abstract های به ارث رسیده بستگی دارند.

در زیر ما از یک interface برای abstract کردن نیاز داریم.

یک اینترفیس به نام IOperation در یک abstract ماژول سطح پایین استفاده شده است.

public interface IOperation
{
  void Initiate();
  void Send();
}

public class LowLevelModule: IOperation
{
  public void Initiate()
  {
    //do initiation before sending
  }
  
  public void Send()
  {
    //perform sending operation
  }
}

دوما  به دلیل اینکه ماژول های سطح بالا صرفا به  Operation abstraction بستگی دارند ، ما نمیتوانیم متد LowLevelModule() را درون کلاس HighLevelModule داشته باشیم.

LowLevelModule نیاز به تزریق شدن در کلاس HighLevelModule از فراخوان کننده محتوا ( caller context) دارند.

وابستگی ، LowLevelModule  ، نیاز به معکوس شدن دارد. اینجا جایی است که اصطلاح «وابسته به معکوس» و «معکوس سازی کنترل» آمده است.

پیاده سازی انتزاع ، LowLevelModule ، یا رفتار بایدبه خارج از  HighLevelModule منتقل شود ، و روند حرکت آن از داخل به خارج از کلاس است که آن را وارونگی میخوانند.

در زیر تزریق وابستگی با Constructor را بررسی میکنیم.

public class HighLevelModule
{
  private readonly IOperation _operation;

  public HighLevelModule(IOperation operation)
  {
    _operation = operation;
  }

   public void Call()
  {
    _operation.Initiate();
    _operation.Send();
  }
}

ما ارتباط HighLevelModule و LowLevelModule را از یکدیگر جدا کردیم و حال هر دوی آنها به abstraction IOperatio بستگی دارند.

ارسال رفتار متد میتواند خارج از کلاس کنترل شود.

با توجه به نکته ی دوم DIP کد ما هنوز کامل نشده است. abstract نباید به جزییات یا پیاده سازی بستگی داشته باشد.

متد Initiate در IOperation در حقیقت پیاده سازی جزییات  LowLevelModule است که ، برای آماده سازی LowLevelModule استفاده شده است، قبل از آن میتوانید عملیات ارسال را انجام دهید.

 خب ما باید آن را به عنوان بخشی از جزییات پیاده سازی LowLevelModule ، از abstraction حذف کنیم .

ما میتوانیم ، عملگر Initiate درون سازنده LowLevelModule ایجاد کنیم.

این اجازه private شدن متد و محدود کردن سطح دسترسی به کلاس را میدهد.

public interface IOperation
{
  void Send();
}

public class LowLevelModule: IOperation
{
  public LowLevelModule()
  {
    Initiate();
  }

  private void Initiate()
  {
    //do initiation before sending
  }
  
  public void Send()
  {
    //perform sending operation
  }
}

public class HighLevelModule
{
  private readonly IOperation _operation;

  public HighLevelModule(IOperation operation)
  {
    _operation = operation;
  }

  public void Call()
  {
    _operation.Send();
  }
}

2) متد های abstract :

اولین اقدام در پیاده سازی DIP اعمال abstraction قسمتی از کد است.

در دنیای #C ، چند راه برای این کار وجود دارد :

1- با استفاده از interface

2- با استفاده از کلاس Abstract

3- با استفاده از Delegate

نخست ، یک اینترفیس منحصرا برای تهیه ی یک abstraction  استفاده میشود، هنگامی که  یک کلاس  abstract میتواند برای تهیه چند اشتراک از جزییات پیاده سازی استفاده شوند.

یک delegate یک abstraction برای قسمتی از تابع یا متد را فراهم میکند.

نکته ای دیگر ،این یک تمرین ساده برای مارک کردن یک متد به عنوان virtual است ، بنابراین متد میتوان مسخره باشد هنگامی که ما از واحد تست برای فراخوانی کلاس  استفاده میکنیم. با این حال این شبیه به استفاده از یک abstraction نیست.

مارک کردن یک متد به عنوان virtual فقط آن را قادر به override شدن میسازد. پس متدها در unit testing هم میتوانند مسخره و هم مفید واقع شوند.

ترجیح ما استفاده از یک interface برای هدف abstraction است.

اگر یک پیاده سازی مشترک جزییات بین دو یا چند کلاس وجود داشته باشد ما فقط از یک کلاس abstract  استفاده میکنیم ،

حتی اگر  ما اطمینان داشته باشیم که کلاس abstract یک interface برای انتزاع واقعی پیاده سازی میکند

در قسمت قبل اول ما یک مثال از استفاده abstraction ها از interface  زدیم اما در این قسمت میخواهیم مثالی با استفاده از یک کلاس abstraction و delegate بزنیم.

با استفاده از کلاس abstract :

استفاده از مثال قست اول ، ما فقط نیاز به تغییر اینترفیس IOperation به یک کلاس abstract به نام OperationBase داریم.

public abstract class OperationBase
{
  public abstract void Send();
}

public class LowLevelModule: OperationBase
{
  public LowLevelModule()
  {
    Initiate();
  }

  private void Initiate()
  {
    //do initiation before sending
  }
  
  public void Send()
  {
    //perform sending operation
  }
}

public class HighLevelModule
{
  private readonly OperationBase _operation;

  public HighLevelModule(OperationBase operation)
  {
    _operation = operation;
  }

  public void Call()
  {
    _operation.Send();
  }
}

کد های بالا معادل استفاده از یک رابط است.

به طور معمول فقط از کلاس های abstract اگر یک اشتراک گذاری جزییات پیاده سازی وجود داشته باشد، استفاده میشود.

برای مثال ، اگر if HighLevelModule بتواند با LowLevelModule استفاده شود  و هر دو کلاس ها یک اشتراک گذاری جزییات پیاده سازی داشته باشند ، آنگاه ما از کلاس abstract به عنوان یک کلاس پایه برای هر دوی آن ها استفاده میکنیم.

کلاس پایه را با IOperation پیاده سازی میکنیم.

public interface IOperation
{
  void Send();
}

public abstract class OperationBase: IOperation
{
  public OperationBase()
  {
    Initiate();
  }

  private void Initiate()
  {
    //do initiation before sending, also shared implementation in this example
  }

  public abstract void Send();
}

public class LowLevelModule: OperationBase
{
 
  public void Send()
  {
    //perform sending operation
  }
}

public class AnotherLowLevelModule: OperationBase
{
  
  public void Send()
  {
    //perform another sending operation
  }
}

public class HighLevelModule
{
  private readonly IOperation _operation;

  public HighLevelModule(IOperation operation)
  {
    _operation = operation;
  }

  public void Call()
  {
    _operation.Send();
  }
}

با استفاده از Delegate :

یک متد یا تابع تک با استفاده از یک  delegate ، میتواند abstract شود .

از <delegate Func<T این میتواند برای این عمل استفاده شود.

public class Caller
{ 
  public void CallerMethod()
  {
    var module = new HighLevelModule(Send);
    ...
  }

  public void Send()
  {
    //this is the method injected into HighLevelModule
  }
}

public class HighLevelModule
{
  private readonly Action _sendOperation;

  public HighLevelModule(Action sendOperation)
  {
    _sendOperation = sendOperation;
  }

  public void Call()
  {
    _sendOperation();
  }
}

و همچنین شما میتوانید delegate  خود را بسازید و یک نام به آن اختصاص دهید.

public delegate void SendOperation();

public class Caller
{ 
  public void CallerMethod()
  {
    var module = new HighLevelModule(Send);
    ...
  }

  public void Send()
  {
    //this is the method injected into HighLevelModule
  }
}

public class HighLevelModule
{
  private readonly SendOperation _sendOperation;

  public HighLevelModule(SendOperation sendOperation)
  {
    _sendOperation = sendOperation;
  }

  public void Call()
  {
    _sendOperation();
  }
}

مزایای استفاده از generic delegateها  این است که دیگر نیازی به ساخت  یا پیاده سازی نوع ، برای مثال اینترفیس و کلاس ها برای وابستگی را نداریم.

3) متد های وابستگی معکوس :

در قسمت اول ،ما از تزریق وابستگی constructor به عنوان متد وابستگی معکوس استفاده کردیم.

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

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

1- استفاده از تزریق وابستگی

2-  استفاده از حالت های عمومی

3-  استفاده از راه های غیر مستقیم

1) استفاده از تزریق وابستگی :

استفاده از تزریق وابستگی ، جایی است که به طور مستقیم ما وابستگی را به عضو های عمومی کلاس تزریق میکنیم

تزریق میتواند به  ،سازنده کلاس نیز تزریق شود(Contructor Injection) میتواند به property تزریق شود (Setter Injection) و همچنین میتواند به متد تزریق شود (Method Injection) , رویداد ها ، index property ها ، فیلد ها و  هر عضو از کلاس که public باشد.

توصیه میشود که از فیلد ها برای تزریق وابستگی استفاده نشود. زیرا آزاد گذاشتن فیلد ها در برنامه نویسی شی گرا کار سنجیده ای نیست.

Constructor Injection  :

ما بیشتر از تزریق به سازنده استفاده کرده ایم. استفاده از  Constructor Injection میتواند قدرت نفوذ بیشتری داشته باشد از قبیل :

wiring خود کار یا کشف نوع.

ما در باره ی IoC container در ادامه صحبت خواهیم کرد.

public class HighLevelModule
{
  private readonly IOperation _operation;

  public HighLevelModule(IOperation operation)
  {
    _operation = operation;
  }

  public void Call()
  {
    _operation.Send();
  }
}

Setter Injection    :

تزریق به setter و متد بعد از تزریق به سازنده استفاده میشود. این میتواند به ما معایب  را  زمانی که از IoC container استفاده میکنیم را نشان دهد.

هرچند که اگر شما از IoC container استفاده نکنید ، ما به همان چیزی میرسیم که با  Constructor Injection میرسیدیم.

یکی دیگر از مزایای تزریق به setter و متد این است که به شما اجازه می دهد برای دادن alter و هشدار تزریق در زمان اجرا را میدهد.

و شما میتوانید به طور کامل از  Constructor injection استفاده کنید.

public class HighLevelModule
{
  public IOperation Operation { get; set; }

  public void Call()
  {
    Operation.Send();
  }
}

Method Injection :

Method Injection به شما اجازه برای قرار دادن چند وابستگی در یک زمان را میدهد.

public class HighLevelModule
{
  private readonly IOperation _operationOne;
  private readonly IOperation _operationTwo;

  public void SetOperations(IOperation operationOne, IOperation operationTwo)
  {
    _operationOne = operationOne;
    _operationTwo = operationTwo;
  }

  public void Call()
  {
    _operationOne.Send();
    _operationTwo.Send();
  }
}

هنگامی که از Method Injection استفاده میکنید ، وابستگی هایی که به عنوان آرگومان ارسال شده اند در کلاس ادامه پیدا میکنند.

به عنوان مثال یک فیلد یا property در بعد استفاده میشود.

استفاده از event ها :

استفاده از رویداد ها به تزریق از نوع delegate محدود است و این فقط زمانی مناسب است که subscription  و notificatin model جزو ملزومات است و delegate قرار نیست هیج مقداری را برگرداند وفقط  void بازمیگرداند.

فراخوان کننده یک delegate در کلاس است event را پیاده سازی میکند که میتواند چندین subscribers باشد.

 event injection میتواند بعد از شی سازنده کار کند.

تزریق رویدادها با سازنده غیرمعمول است.

مثالی از  event injection :

public class Caller
{ 
  public void CallerMethod()
  {
    var module = new HighLevelModule();
    module.SendEvent += Send ;

    ...
  }

  public void Send()
  {
    //this is the method injected into HighLevelModule
  }
}

public class HighLevelModule
{
  public event Action SendEvent = delegate {};

  public void Call()
  {
    SendEvent();
  }
}

2) استفاده از حالت های عمومی

به جای تزریق به صورت مستقیم به کلاس ، وابستگی میتواند از یک  global state از داخل کلاس بازیابی شود.

وابستگی میتواند درون global state ها تزریق شود.

public class Helper
  {
    public static IOperation GlobalStateOperation { get; set;}
  }

  public class HighLevelModule
  {
    public void Call()
    {
       Helper.GlobalStateOperation.Send();
    }
  }
   
  public class Caller
  {
    public void CallerMethod()
    {
      Helper.GlobalStateOperation = new LowLevelModule();

      var highLevelModule = new HighLevelModule();
      highLevelModule.Call();
    }
  }  
}

Global state ها میتواند به عنوان یک property ، متد ها یا حتی فیلد ها نمایش داده شوند.

نکته مهم آن است که مقدار های پایه ای باید public باشند.

setter و getter میتواند به جایproperty در فرم متد ها باشند.

اگر global state  اگر فقط دارای getter باشد ، وابستگی معکوس نیست. با استفاده از  global state  برای وابستگی معکوس توصیه نمیشود.

3)  استفاده از راه های غیر مستقیم (Indirection)

اگر شما از Indirection استفاده میکنید ، شما نباید به صورت مستقیم وابستگی ها را به کلاس تزریق کنید.

در عوض برای شما یک شی را که قادر به ارسال به abstraction دارد، ایجاد میکند.

این به معنی است که شما یک وابستگی دیگر برای کلاس ساخته اید.

نوع شی ارسالی شما میتواند یکی از دو نوع زیر باشد :

•شی registery/Container 

• شی factory 

شی  :registery/Container :

اگر شما از یک register استفاده کرده اید ، این اغلب الگوی Service Locator نامیده میشود.سپس شما میتوانید query ثبت را  برای برگشت پیاده سازی یک abstraction  را پیاده سازی کنید.

هرچند که شما نیاز به register  و پیاده سازی در خارج از کلاس دارید. شما همچنین میتوانید از یک container استفاده کنید.

یک container معمولا  چند ویژگی  از قبیل  یا auto-wiring را دارد. هنگامی که شما یک اینترفیس را register میکنید ، شما به پیاده سازی کلاس وابستگی نیاز ندارید.

وقتی شما query میزنید container قادر به بازگرداندن کلاس پیاده سازی وابستگی است.

public interface IOperation
{
  void Send();
}

public class LowLevelModule: IOperation
{
  public LowLevelModule()
  {
    Initiate();
  }

  private void Initiate()
  {
    //do initiation before sending
  }
  
  public void Send()
  {
    //perform sending operation
  }
}

public class HighLevelModule
{
  private readonly Container _container;

  public HighLevelModule(Container container)
  {
    _container = container;
  }

  public void Call()
  {
    IOperation operation = _container.Resolvel&lt;IOperation>();
    operation.Send();
  }
}

public class Caller
{
  public void UsingContainerObject()
  {
     //registry the LowLevelModule as implementation of IOperation
     var register  = new Registry();
     registry.For&lt;IOperation>.Use&lt;LowLevelModule>();

     //wrap-up registry in a container
     var container = new Container(registry);
      
     //inject the container into HighLevelModule
     var highLevelModule = new HighLevelModule(container);
     highLevelModule.Call();     
  }
}

شی factory :

فرق بین استفاده از شی register/Container و Factory این است که هنگامی که ما از یک register/Container استفاده میکنیم شما نیاز به پیاده سازی کلاس قبل از زدن query دارید.

زمانی که از شی factory استفاده میکنید شما نیاز به آن کار ندارید.

شی factory لزومی به داشتن قسمتی با نام 'factory'  ندارد.

ماژول های سطح پایین هارد کد هستند.

داشتن ماژول های سطح بالا به factory وابسته هستند و به ماژول های سطح پایین بستگی ندارند برای معکوس کردن وابستگی ها ماژول های سطح بالا  ، نیاز به abstraction دارد.

در زیر یک مثال از شی factory داریم :

public interface IOperation
{
  void Send();
}


public class LowLevelModule: IOperation
{
  public LowLevelModule()
  {
    Initiate();
  }

  private void Initiate()
  {
    //do initiation before sending
  }
  
  public void Send()
  {
    //perform sending operation
  }
}

public interface IModuleFactory
{
   IOperation CreateModule();
}

public class ModuleFactory: IModuleFactory
{
  public IOperation CreateModule()
  {
      //LowLevelModule is the implementation of the IOperation, 
      //and it is hardcoded in the factory. 
      return new LowLevelModule();
  }
}

public class HighLevelModule
{
  private readonly IModuleFactory _moduleFactory;

  public HighLevelModule(IModuleFactory moduleFactory)
  {
    _moduleFactory = moduleFactory;
  }

  public void Call()
  {
    IOperation operation = _moduleFactory.CreateModule();
    operation.Send();
  }
}

public class Caller
{
  public void CallerMethod()
  {
     //create the factory as the implementation of abstract factory
     IModuleFactory moduleFactory = new ModuleFactory();
      
     //inject the factory into HighLevelModule
     var highLevelModule = new HighLevelModule(moduleFactory);   
     highLevelModule.Call();  
  }
}

توصیه ما این است که از روش مستقیم کمتر استفاده کنید.

الگوی service locator  امروزه یک (anti pattern) ضد الگو است.

دانلود نسخه ی PDF این مطلب