تزریق وابستگی در Class Libraries

در این مقاله قصد داریم چگونگی استفاده از (اصل وارونگی کنترل) inversion of control principleهنگام ایجاد و نشر کلاسها برای دیگر توسعه دهندگان را توضیح دهیم.

 تزریق وابستگی در Class Libraries

در این مقاله قصد داریم چگونگی استفاده از (اصل وارونگی کنترل)  inversion of control  principle هنگام ایجاد و نشر کلاسها برای دیگر توسعه دهندگان را توضیح دهیم.

هنگام طراحی فریم ورک خود همیشه می خواهید  اینترفیس یا رابط های مستقل و کلاس هایی برای مشتری حاضر کنید. به طور مثال یک کلاس با نام  ModelService  دارید که یک کلاس ساده برای دسترسی به داده ها است که از یک فریم ورک با نام  SimpleORM منتشر شده است.  هنگامی که مسئولیت ها را تقسیم میکنید و رابط های خود را تفکیک میکنید ، طراحی خود را جایی که کلاس ModelService   از اینترفیسهای دیگر با نام های  IConnectionStringFactory ،  IDTOMapper ، IValidationService  استفاده میکند توسعه می دهید.

تزریق اینترفیس های وابسته به کلاس  ModelService  به طوری که بتوان آن را تست کرد ، براحتی از طریق  Constructor injection   تزریق سازنده بدست می آید.

public ModelService(IConnectionStringFactory factory, IDTOMapper mapper, IValidationService validationService)
{
    this.factory = factory;
    this.mapper = mapper;
    this.validationService = validationService;
}

این نوع از تزریق وابستگی ها غالبا زمانی استفاده می شود که ماژول ها خود را در  برنامه گرد آورید و نمی خواهید آنها را به عنوان یک کتابخانه مستقل نشر دهید. برای نمونه ای از  کلاس  ModelService   تزریق وابستگی ها یا DI container را جستجو خواهیم کرد . در اینجا باید از  IConnectionStringFactory ،  IDTOMapper  و  IvalidationService یا هر اتصال دیگری مطلع باشیم.

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

var modelService = new ModelService(); 

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

ساده ترین راه برای دستیابی به قابلیت تست و ترک سازنده بدون پارامتر برای فراخواننده فریم ورک به صورت زیر می باشد:

public ModelService() : this(new ConnectionStringFactory(), new DTOMapper(), new ValidationService()
{
   // no op
} 

internal ModelService(IConnectionStringFactory factory, IDTOMapper mapper, IValidationService validationService)
{
    this.factory = factory;
    this.mapper = mapper;
    this.validationService = validationService;
}  

با فرض اینکه کلاس  ModelService  خود را در پروژه  تست جداگانه ای تست میکنید، بخاطر داشته باشید که صفت  InternalVisibleTo را درون فایل ویژگی های  SimpleORM تنظیم کنید.

[assembly: InternalsVisibleTo("SimpleORM.Test")]

استفاده از این روش مزیت هایی به همراه دارد :  پنهان کردن سازنده به همراه پارامترهایش از فریم ورک کاربر و اجازه وارد کردن داده های نامعتبر برای تست .

[TestInitialize]
public void SetUp()
{
      var factory = new Mock<IConnectionStringFactory>();
      var dtoMapper = new Mock<IDTOMapper>();
      var validationService = new Mock<ivalidationservice>();

      modelService = new ModelService(factory.Object, dtoMapper.Object, validationService.Object);
}

روش بالا یک مشکل واضح دارد : کلاس  ModelService شما یک وابستگی مستقیم بر کلاسهای مرکب ConnectionStringFactory , DTOMapper و  ValidationService دارد . این نقض loose coupling  یا اصل اتصال سست ، کلاس  ModelService  را به صورت استاتیک وابسته بر اجرای سرویسها ایجاد میکند. برای دور شدن از این وابستگی ها اضافه کردن یک  ServiceLocator  که مسئول نمونه شیء خواهد بود پیشنهاد می شود :

internal interface IServiceLocator
{
    T Get<T>();
}
 
internal class ServiceLocator
{
   private static IServiceLocator serviceLocator;
   
   static ServiceLocator()
   {
        serviceLocator = new DefaultServiceLocator();
   }

   public static IServiceLocator Current
   {
      get
      {
           return serviceLocator;
      }
   }

   private class DefaultServiceLocator : IServiceLocator
   {
      private readonly IKernel kernel;  // Ninject kernel
      
      public DefaultServiceLocator()
      {
          kernel = new StandardKernel();
      }

      public T Get<T>()
      {
           return kernel.Get<T>();
      }
   }
}

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

public ModelService() : this(
ServiceLocator.Current.Get<IConnectionStringFactory>(), 
ServiceLocator.Current.Get<IDTOMapper>(), 
ServiceLocator.Current.Get<IValidationService>())
{
   // no op
} 

باید برای IConnectionStringFactory و  IDTOMapper  و  IValidationService اتصالات پیش فرضی را تعریف کنید.

internal class ServiceLocator
{
   private static IServiceLocator serviceLocator;
   
   static ServiceLocator()
   {
        serviceLocator = new DefaultServiceLocator();
   }

   public static IServiceLocator Current
   {
      get
      {
           return serviceLocator;
      }
   }

   private sealed class DefaultServiceLocator : IServiceLocator
   {
      private readonly IKernel kernel;  // Ninject kernel
      
      public DefaultServiceLocator()
      {
          kernel = new StandardKernel();
          LoadBindings();
      }

      public T Get<T>()
      {
           return kernel.Get<T>();
      }
    
      private void LoadBindings()
      {
          kernel.Bind<IConnectionStringFactory>().To<ConnectionStringFactory>().InSingletonScope();
          kernel.Bind<IDTOMapper>().To<DTOMapper>().InSingletonScope();
          kernel.Bind<IValidationService>().To<ValidationService>().InSingletonScope();
      } 
   } 
}

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

همانطور که همچنان در حال توسعه چهارچوب SimpleORM خود هستید ، تقسیم کتابخانه به زیر ماژول های مختلف به پایان خواهد رسید. فرض کنید می خواهید برای یک کلاس،  گسترشی را فراهم کنید که یک تعامل را با دیتابیس  NoSQL  پیاده سازی میکند . شما نمیخواهید که چهارچوب  SimpleORM شما با وابستگی های غیر ضروری بهم ریخته شود ، بنابراین ماژول  SimpleORM.NoSQL را جداگانه ایجاد میکنیم.  دسترسی به تزریق وابستگی چگونه می شود؟ چگونه می توان اتصالات بیشتری را به هسته Ninject  اضافه کرد؟

در زیر راه حلی ساده برای آن نوشته ایم. اینترفیسی با نام  IModuleLoader  را در کتابخانه کلاس اولیه  SimpleORM  تعریف میکنیم.

public interface IModuleLoader
{
    void LoadAssemblyBindings(IKernel kernel);
}

بجای اتصال مستقیم اینترفیس به پیاده سازی واقعی آن در کلاس  SrviceLocator  ،  کلاس  IModuleLoader را پیاده سازی و اتصالا ت را فراخوانی میکنیم.

internal class SimpleORMModuleLoader : IModuleLoader
{
   void LoadAssemblyBindings(IKernel kernel)
   {
      kernel.Bind<IConnectionStringFactory>().To<ConnectionStringFactory>().InSingletonScope();
      kernel.Bind<IDTOMapper>().To<DTOMapper>().InSingletonScope(); 
      kernel.Bind<IValidationService>().To<ValidationService>().InSingletonScope();
   }
}

اکنون LoadAssemblyBindings از کلاس Service Locator  فراخوانی شد. نمونه ساخته شده از این کلاس ها  ، پاسخی به فراخوانی می باشد.

internal class ServiceLocator
{
   private static IServiceLocator serviceLocator;

   static ServiceLocator()
   {
        serviceLocator = new DefaultServiceLocator();
   }

   public static IServiceLocator Current
   {
      get
      {
           return serviceLocator;
      }
   }

   private sealed class DefaultServiceLocator : IServiceLocator
   {
      private readonly IKernel kernel;  // Ninject kernel
      
      public DefaultServiceLocator()
      {
          kernel = new StandardKernel();
          LoadAllAssemblyBindings();
      }

      public T Get<T>()
      {
           return kernel.Get<T>();
      }
    
     private void LoadAllAssemblyBindings()
     {
         const string MainAssemblyName = "SimpleORM";
         var loadedAssemblies = AppDomain.CurrentDomain
                               .GetAssemblies()
                               .Where(assembly => assembly.FullName.Contains(MainAssemblyName));

        foreach (var loadedAssembly in loadedAssemblies)
        {
              var moduleLoaders = GetModuleLoaders(loadedAssembly);
              foreach (var moduleLoader in moduleLoaders)
              {
                  moduleLoader.LoadAssemblyBindings(kernel);
              }
         }
     }

     private IEnumerable<IModuleLoader> GetModuleLoaders(Assembly loadedAssembly)
     {
        var moduleLoaders = from type in loadedAssembly.GetTypes()
                                      where type.GetInterfaces().Contains(typeof(IModuleLoader))
                                      type.GetConstructor(Type.EmptyTypes) != null
                                      select Activator.CreateInstance(type) as IModuleLoader;
      return moduleLoaders;
     }
}

این کد همه  assembly  های بارگذاری شده در  AppDomain  برای پیاده سازی  IModuleLoader  جستجو میکند. و هنگامی که آن را یافت کرد ، هسته  Singleton شما را به نمونه خود میفرستد، از استفاده شدن  container مشابه در ماژول ها اطمینان یابید.

فریم ورک  SimpleORM.NoSQL شما کلاس  IModuleLoader خود را اجرا میکند که با اولین فراخوانی کلاس ServiceLocator  فراخوانی و نمونه سازی خواهد شد. کد بالا بر وابستگی  SimpleORM.NoSQLبه SimpleORM دلالت دارد که ماژول های گسترش یافته را به والد های خود وابسته میکند.

روش توضیح داده اشکالاتی دارد : مدیریت منابع یکبار مصرف ،  rebinding تصادفی در ماژول های وابسته ، سربار عملکرددر ایجاد و ... که باید با احتیاط با یک مجموعه تست استفاده شوند.

آموزش سی شارپ

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