В статье показывается пример тесно связанных классов, проводиться их рефакторинг с реализацией паттерна Dependency Injection (внедрение зависимости) и демонстрируется применение фреймворка Ninject, облегчающего внедрение зависимостей.

public class Engine
{
   public double GetSize()
   {
      return 2.5; // in liters
   }
}
 
public class Car
{
   private readonly Engine _engine;
 
   public Car()
   {
      _engine = new Engine();
   }
 
   public void GetDescription()
   {
      Console.WriteLine(“Engine size: {0}”, _engine.GetSize());
   }
}
 
class Program
{
   static void Main()
   {
      Car car = new Car();
      car.GetDescription();
   }
}

Каждый из нас писал подобный код хотя бы раз в жизни. А те, в чьи обязанности входит проверка кода начинающих программистов, могут видеть это достаточно часто. С синтаксической и логической точек зрения в нем нет ничего криминального. Код отлично отработает, выполнив все от него ожидаемое, и в нем абсолютно ничего не надо трогать, если это часть программы состоящей из пяти классов. Вопросы возникают, когда такой код появляется в реальном проекте, насчитывающем десятки классов, имеющем длинный жизненный цикл и большую вероятность изменения пользовательских требований, способа хранения данных или поддерживаемого оборудования.

Чем же он плох? Если кратко, то зависимостью конкретного класса Car от конкретного класса Engine. Представим, что конструктор класса Engine измениться – в этом случае придется менять и Car. Или, к примеру, появиться еще один двигатель. И что бы машина смогла его использовать, опять-таки, понадобиться изменить класс Car. Кроме того, подобная жестко запрограммированная зависимость снижает тестируемость классов – класс Car невозможно протестировать отдельно от Engine или заменить Engine на ложный объект, для симуляции непредвиденных обстоятельств.

Что же можно сделать для улучшения кода? Первое – ввести интерфейс IEngine, который будет реализовываться классом Engine.

public interface IEngine
{
   double GetSize();
}
 
public class Engine : IEngine
{
   public double GetSize()
   {
      return 2.5; // in liters
   }
}

Далее, изменить класс Car, что бы вместо класса Engine в нем использовался интерфейс IEngine. И второе, не менее важное изменение – класс Car не должен создавать двигатель сам, теперь конструктор получает ссылку на объект, реализующий интерфейс IEngine.

public class Car
{
   private readonly IEngine _engine;
 
   public Car(IEngine engine)
   {
      _engine = engine;
   }
 
   public void GetDescription()
   {
      Console.WriteLine(“Engine size: {0}L”, _engine.GetSize());
   }
}

И последнее, что необходимо сделать – это изменить функцию Main.

class Program
{
   static void Main()
   {
      IEngine engine = new Engine();
      Car car = new Car(engine);
 
      car.GetDescription();
   }
}

Получившееся в результате решение легче расширяемо, более тестируемо и является примером реализации паттерна Dependency Injection (внедрение зависимости) – зависимость машина-двигатель конфигурируется не внутри класса, а двигатель, созданный вне класса, внедряется в него.

Минус такого решения – потерялась простота создания нового экземпляра класса Car. Представьте, если в программе достаточно много подобных зависимостей, их все придется создавать вручную снова и снова. Поэтому, мы пойдем дальше и посмотрим, как можно облегчить жизнь используя Ninject, фреймворк для автоматического внедрения зависимостей.

Ninject

Основную идею использования DI фреймворков для создания объектов можно описать так: “Мне нужен объект класса A, создай его, и меня не интересует, что и как ты для этого будешь делать”. И вот как будет выглядеть создание машины с использованием Ninject:

class Program
{
   static void Main()
   {
      // Ninject Initialization
      IKernel ninjectKernel = new StandardKernel( new MyConfigModule() );
 
      // Using Car
      Car car = ninjectKernel.Get();
      car.GetDescription();
   }
}

Что такое ninjectKernel станет понятно чуть позже, а сейчас стоит обратить внимание на отсутствие любого упоминания IEngine или Engine. Благодаря предварительной конфигурации Ninject знает, что машине требуется двигатель и когда его просят создать новый экземпляр класса Car, он самостоятельно создает Engine и передает его в конструктор Car. Таким образом, все что требуется – это запросить класс. Все зависимости, необходимые для его создания или работы, DI фреймворк разрешит самостоятельно. Если, конечно, он был предварительно правильно сконфигурирован.

Ninject можно или загрузить с сайта Ninject.org в виде стабильной версии, или взять в виде актуальных исходных файлов из репозитория. Я рекомендую воспользоваться вторым способом, так как версия, находящаяся в репозитории, поддерживает несколько новых возможностей, в том числе код для интеграции с ASP.NET MVC.

Для начала работы с Ninject, в проект необходимо добавить ссылку на сборку Ninject.Core.dll, реализующую базовые возможности фреймворка. Второй шаг – конфигурирование. К сожалению, Dependency Injection – это не магия и что бы Ninject знал, каким образом разрешать зависимости, он должен быть проинструктирован. Для нашей программы необходимы только две инструкции:

Bind().To();
Bind().ToSelf();

В то время как многие DI фреймворки полагаются на конфигурационные XML файлы, Ninject имеет простой, интуитивно понятный API для задания зависимостей, использующий привычный синтаксис языка и позволяющий задействовать все возможности IDE.

Первая инструкция связывает интерфейс IEngine с классом Engine таким образом, что всякий раз, когда Ninject обнаружит необходимость в объекте типа IEngine, то будет создан объект Engine. Вторая инструкция описывает желаемое поведение при запросе на создание экземпляра класса Car – необходимо создать этот экземпляр, все просто. На самом деле, вторая инструкция не обязательна – такое поведение реализуется фреймворком по умолчанию. При необходимости автосвязывание классов можно и отключить, и настраивать все самому.

Теперь возникает вопрос – а где выполнять конфигурирование? Конфигурирование выполняется в виртуальном методе Load класса, реализующего интерфейс IModule. Проще всего будет наследовать такой класс от класса StandardModule.

public class MyConfigModule : StandardModule
{
   public override void Load()
   {
      Bind().ToSelf();
      Bind().To();
   }
}

При желании, может быть создано несколько таких модулей, например для разделения конфигураций для различных частей программы. Далее эти модули используются для создания контейнера, который и будет выполнять для нас всю грязную работу.

IKernel kernel = new StandardKernel(cfg1, cfg2, …);
blog comments powered by Disqus