Pull to refresh

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

Reading time 6 min
Views 20K
Продолжая тему 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 и прочие интересности обсудим в следующий раз.
Tags:
Hubs:
+3
Comments 3
Comments Comments 3

Articles