4 июля 2009 в 13:27

DI и IoC для начинающих, часть 3

.NET*
Продолжая тему DI/IoC с использованием Unity (часть 1, 2), мы рассмотрим как можно им воспользоваться в ситуациях, когда объект создаем не мы, а также посмотрим на применение фреймворка при unit-тестировании.

Ситуация, в которой регистрируется несколько мэппингов одного типа поддается контролю когда нужно передать все зарегистрированные типы сервисов (то есть IService[]). Но что если нужно получить один конкретный сервис из контейнера? Для этого в Unity предусмотрена возможность давать объектам имена. Например, чтобы реализовать аналог вот этого кода
var svc10 = new MyService(10);<br/>
var svc15 = new MyService(15);<br/>
нужно зарегистрировать как раз “именные” мэппинги, а точнее:
var uc = new UnityContainer();<br/>
// регистрируем с именем
uc.RegisterType<IService, MyService>("ten",<br/>
  new InjectionConstructor(new InjectionParameter(10)))<br/>
  .RegisterType<IService, MyService>("fifteen",<br/>
  new InjectionConstructor(new InjectionParameter(15)));<br/>
// получаем
var _10 = uc.Resolve<IService>("ten");<br/>
var _15 = uc.Resolve<IService>("fifteen");<br/>
Также, имеется возможность получить все зарегистрированные мэппинги. Делется это с помощью функции ResolveAll():
foreach (IService svc in uc.ResolveAll<IService>())<br/>
  svc.DoSomething();<br/>

Внешнее создание


Бывают ситуации, когда объект, для которого нужно делать DI, создается вне нашего контроля. Примеров где это происходит много – это WCF, WPF, различные remoting/SoA сценарии. Несмотря на то, что объект сделан кем-то еще, все равно неплохо иметь возможность подключать его (а также зависимые объекты) в общий процесс.

Вот еще один синтетический пример:
public class HelperClass<br/>
{<br/>
  public void DoSomething()<br/>
  {<br/>
    Console.WriteLine("Doing something");<br/>
  }<br/>
}<br/>
public class SomeService<br/>
{<br/>
  [Dependency]<br/>
  public HelperClass MyHelperClass { get; set; }<br/>
  public void Go() { MyHelperClass.DoSomething(); }<br/>
}<br/>
 <br/>
⋮<br/>
 <br/>
var uc = new UnityContainer();<br/>
var ss = Activator.CreateInstance<SomeService>();<br/>
Console.WriteLine(ss.MyHelperClass == null);  // True
Получив объект уже готовым, DI механизм не был применен, но это не проблема – его можно получить post-factum используя метов BuildUp():
var uc = new UnityContainer();<br/>
// симулируем "внешнее" создание объекта
var ss = Activator.CreateInstance<SomeService>();<br/>
Console.WriteLine(ss.MyHelperClass == null);  // True
var ss2 = uc.BuildUp<SomeService>(ss);<br/>
Console.WriteLine(ss2.MyHelperClass == null); // False
ss2.Go();<br/>
Console.WriteLine(ReferenceEquals(ss, ss2));  // True
Как можно видеть из кода, метод BuildUp() “надстраивает” на существующем объекте те зависимости, которые были бы созданы если бы объект выдавал IoC контейнер. Наш пример также демонстрирует, что при надстройке нового объекта не создается – используется старый.

Конечно, если объект создается извне, меняется поведение метода Resolve(). В частности, если наш сервис реализует IService, то мы все равно можем делать мэппинг с IService на существующий объект, и в последствии выдавать эту же копию без проблем. Для этого, мы регистрируем существующий объект с помощью метода RegisterInstance():
var uc = new UnityContainer();<br/>
var ss = Activator.CreateInstance<SomeService>();<br/>
uc.BuildUp<SomeService>(ss);<br/>
// при запросах IService выдается именно ss
uc.RegisterInstance<IService>(ss);<br/>
var svc = uc.Resolve<IService>();<br/>
svc.Go();<br/>

Тестирование


Она из целей механизма DI – упростить unit-тестирование. Суть в том, что для большинства разработчиков, unit ≡ класс, и поэтому другие классы в тестах нужно заменять. Возьмем еще один простой пример:
public interface IAdder<br/>
{<br/>
  int Add(int first, int second);<br/>
}<br/>
public sealed class AdderService : IAdder<br/>
{<br/>
  public int Add(int first, int second)<br/>
  {<br/>
    return first + second;<br/>
  }<br/>
}<br/>
public class MyApp<br/>
{<br/>
  private IAdder adder;<br/>
  public MyApp(IAdder adder)<br/>
  {<br/>
    this.adder = adder;<br/>
  }<br/>
  public int AddAndMultiply(int first, int second, int third)<br/>
  {<br/>
    return adder.Add(first, second) * third;<br/>
  }<br/>
}<br/>
Следующий тест является интеграционным, т.к. в нем задействованы конкретные копии двух классов – AdderService и MyApp:
[Test]<br/>
public void NotAUnitTest()<br/>
{<br/>
  var uc = new UnityContainer();<br/>
  uc.RegisterType<IAdder, AdderService>();<br/>
  Assert.AreEqual(20, uc.Resolve<MyApp>().AddAndMultiply(2, 3, 4));<br/>
}<br/>
В рабочем коде, между прочим, контейнер был бы сконфигурирован именно так, как показано выше. А вот при тестировании, мы можем подменить AdderService другой, “тестовой” реализацией. Самый простой вариант – написать некий FakeAdderService который просто возвращает константу:
public class FakeAdderService : IAdder<br/>
{<br/>
  public int Add(int first, int second)<br/>
  {<br/>
    return 5; // константа!
  }<br/>
}<br/>
Теперь меняем контейнер и – ура! – у нас полноценный unit-тест:
[Test]<br/>
public void UnitTestWithFake()<br/>
{<br/>
  var uc = new UnityContainer();<br/>
  uc.RegisterType<IAdder, FakeAdderService>();<br/>
  Assert.AreEqual(20, uc.Resolve<MyApp>().AddAndMultiply(2, 3, 4));<br/>
}<br/>
Конечно, наш фейк – штука на особо гибкая – ведь он возвращает именно то значение, которое мы ожидаем при вводе (то есть в тесте мы пишем “2, 3” а в фейке возвращаем число 5). И если вдруг нам потребуется в другом тесте использовать другие значения, что делать тогда? Делать еще один фейк-объект? Или делать наш фейк параметризуемым и конфигурировать его параметры в контейнере?

К счастью ничего подобного делать не нужно. Вместо этого, можно воспользоваться мок-фреймворком, таким как например Rhino Mocks или Typemock Isolator. Тогда можно без собственных фейк-классов переписать наш тест так, чтобы в него автоматически подставлялся поддельный объект типа IService, который возвращал бы именно то значение, которое нужно нам:
var uc = new UnityContainer();<br/>
var svc = Isolate.Fake.Instance<AdderService>();<br/>
Isolate.WhenCalled(() => svc.Add(0, 0)).WillReturn(5);<br/>
uc.RegisterInstance<IAdder>(svc);<br/>
Assert.AreEqual(20, uc.Resolve<MyApp>().AddAndMultiply(2, 3, 4));<br/>
Выше я создал поддельный объект с помощью Typemock (почитать про него можно тут), задал поведение для вызова метода Add() (всегда возвращать 5), и добавил его в контейнер. Тем самым получил полноценный, легко конфигурируемый unit-тест.

Вот пока и все. Automocking и прочие интересности обсудим в следующий раз.
Дмитpий Hecтepук @mezastel
карма
109,8
рейтинг –0,1
Пользователь
Похожие публикации
Самое читаемое Разработка

Комментарии (3)

  • 0
    Блин, вот хоть убейте, не понимаю, зачем всё это нужно. Видимо я до этого ещё не дорос :o)
    • 0
      Всему свое время, обязательно дорастете.
    • 0
      Советую изучить хотя бы общие азы, т.к. сейчас тему DI/IoC спрашивают на собеседованиях на работу — независимо от того, используете ли вы Java, .Net или что-нибудь еще.

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.