Pull to refresh

Создание простейшего DI контейнера с использованием TDD

Reading time4 min
Views8.8K

Введение



Сегодня просмотрел ряд скринкастов от Daniel Cazzulino, в которых он рассказывает о создании с нуля простейшего DI контейнера, что не могло не привлечь моего внимания. Ниже будут приведены примеры из его скринкастов.

Подробнее o IoC/DI контейнерах можно прочитать здесь

Требования



Нам понадобится Visual Studio 2008, 15-20 минут свободного времени и желание узнать как же сделать свой DI контейнер.

Начнём. Funq1



  • Для начала создадим проект Class Library в Visual Studio, назовём его Funq1
  • Далее добавим в решение проект тестов (мы будем использовать TDD на базе тестов Visual Studio) и назовём его Funq1.Tests, создадим ссылку из него на наш проект Funq1


Funq1.Tests



  • В проекте Funq1.Tests создадим тестовый класс ContainerFixture, который будет проверять функционал нашего контейнера.
  • Добавим объявление интерфейсов IFoo, IBar, а так же соответственно классов Foo, Bar, для тестирования нашего контейнера:


  1. public interface IBar { }
  2. public interface IFoo { }
  3.  
  4. public class Bar : IBar { }
  5.  
  6. public class Foo : IFoo
  7. {
  8.     public IBar Bar { get; private set; }
  9.  
  10.     public Foo(IBar bar)
  11.     {
  12.         Bar = bar;
  13.     }
  14. }

* This source code was highlighted with Source Code Highlighter.


  • Создадим тестовый метод, который проверит умеет ли наш контейнер регистрировать и возвращать объекты, выглядеть он будет примерно следующим образом:


  1. [TestMethod]
  2. public void RegisterTypeAndGetInstance()
  3. {
  4.     var container = new Container();
  5.  
  6.     container.Register<IBar>(() => new Bar());
  7.  
  8.     var bar = container.Resolve<IBar>();
  9.  
  10.     Assert.IsNull(bar);
  11.     Assert.IsTrue(bar is Bar);
  12. }

* This source code was highlighted with Source Code Highlighter.


  • Итак, на тестовый класс ContainerFixture выглядит следующим образом:


  1. using Microsoft.VisualStudio.TestTools.UnitTesting;
  2.  
  3. namespace Funq1.Tests
  4. {
  5.     [TestClass]
  6.     public class ContainerFixture
  7.     {
  8.         [TestMethod]
  9.         public void RegisterTypeAndGetInstance()
  10.         {
  11.             var container = new Container();
  12.  
  13.             container.Register<IBar>(() => new Bar());
  14.  
  15.             var bar = container.Resolve<IBar>();
  16.  
  17.             Assert.IsNull(bar);
  18.             Assert.IsTrue(bar is Bar);
  19.         }
  20.  
  21.         public interface IBar { }
  22.         public interface IFoo { }
  23.  
  24.         public class Bar : IBar { }
  25.  
  26.         public class Foo : IFoo
  27.         {
  28.             public IBar Bar { get; private set; }
  29.  
  30.             public Foo(IBar bar)
  31.             {
  32.                 Bar = bar;
  33.             }
  34.         }
  35.     }
  36. }

* This source code was highlighted with Source Code Highlighter.


Funq1.Container



  • Наш тест не проходит, так как Container остается не объявленный, в связи с этим в проекте Funq1 добавляем класс Container
  • Объявляем поле, в котором будут храниться соответствия типов:


  1. Dictionary<Type, object> factories = new Dictionary<Type, object>();

* This source code was highlighted with Source Code Highlighter.


  • Добавим метод для регистрации типа в контейнере:


  1. public void Register<TService>(Func<TService> factory)
  2. {
  3.     factories.Add(typeof(TService), factory);
  4. }

* This source code was highlighted with Source Code Highlighter.


  • А так же метод, для извлечения типа из контейнера:


  1. public TService Resolve<TService>()
  2. {
  3.     object factory = factories[typeof(TService)];
  4.  
  5.     return ((Func<TService>)factory).Invoke();
  6. }

* This source code was highlighted with Source Code Highlighter.


  • На данный момент наш Container выглядит следующим образом:


  1. using System;
  2. using System.Collections.Generic;
  3.  
  4. namespace Funq1
  5. {
  6.     public class Container
  7.     {
  8.         Dictionary<Type, object> factories = new Dictionary<Type, object>();
  9.  
  10.         public void Register<TService>(Func<TService> factory)
  11.         {
  12.             factories.Add(typeof(TService), factory);
  13.         }
  14.  
  15.         public TService Resolve<TService>()
  16.         {
  17.             object factory = factories[typeof(TService)];
  18.  
  19.             return ((Func<TService>)factory).Invoke();
  20.         }
  21.     }
  22. }

* This source code was highlighted with Source Code Highlighter.


Funq1.Tests



  • Пробуем запустить тесты, но они не проходят, т.к. мы умышленно внесли ошибку  в описании метода RegisterTypeAndGetInstance() в строке 17: Assert.IsNull(bar), чтобы убедиться, что тест срабатывает
  • Для того, чтобы тест прошёл, следует изменить эту строчку на Assert.IsNotNull(bar);
  • Далее нас интересует, а что же с зависимостью в конструкторе, как её необходимо зарегистрировать, для этого мы объявляем тестовый метод ResolveGetsDepedenciesInjected() и заносим следующие объявления:


  1. container.Register<IBar>(() => new Bar());
  2. container.Resolve<IFoo>(() => new Foo(container.Resolve<IBar>()));

* This source code was highlighted with Source Code Highlighter.


  • Однако данное объявление не понравится нашему компилятору, т.к. в области видимости замыкания не присутствует container, в связи с этим данное объявление придётся немного изменить:


  1. container.Register<IBar>(c => new Bar());
  2. container.Register<IFoo>(c => new Foo(c.Resolve<IBar>()));

* This source code was highlighted with Source Code Highlighter.


Funq1



  • Соответственно необходимо будет немного изменить наш контейнер, добавив в функции аргумент Container:


  1. public void Register<TService>(Func<Container, TService> factory)
  2. {
  3.     factories.Add(typeof(TService), factory);
  4. }
  5.  
  6. public TService Resolve<TService>()
  7. {
  8.     object factory = factories[typeof(TService)];
  9.  
  10.     return ((Func<Container, TService>)factory).Invoke(this);
  11. }

* This source code was highlighted with Source Code Highlighter.


Funq1.Tests



  • После внесенных в контейнер изменений необходимо будет немного поправить наш первый тестовый метод RegisterTypeAndGetInstance() строку 13 на выражение:


  1. container.Register<IBar>(c => new Bar());

* This source code was highlighted with Source Code Highlighter.


  • Далее вернемся к нашему методу ResolveGetsDepedenciesInjected() и добавим к нему извлечение объекта и утверждение-проверку на null, в результате получим:


  1. [TestMethod]
  2. public void ResolveGetsDepedenciesInjected()
  3. {
  4.     var container = new Container();
  5.  
  6.     container.Register<IBar>(c => new Bar());
  7.     container.Register<IFoo>(c => new Foo(c.Resolve<IBar>()));
  8.  
  9.     var foo = container.Resolve<IFoo>() as Foo;
  10.  
  11.     Assert.IsNotNull(foo);
  12.     Assert.IsNotNull(foo.Bar);
  13. }

* This source code was highlighted with Source Code Highlighter.


Заключение



Вот и подошёл к концу наш увлекательный (надеюсь) урок по созданию с нуля простейшего DI контейнера, что же мы приобрели в процессе:

  1. Базовое понимание функционирования DI контейнера
  2. Практические навыки разработки с использованием TDD
Tags:
Hubs:
+5
Comments8

Articles