Паттерн Dependency Injection и введение в Ninject – DI фреймворк для .NET

.NET, C# 2 Comments

В статье показывается пример тесно связанных классов, проводиться их рефакторинг с реализацией паттерна 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>();
car.GetDescription();
}
}

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

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

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

Bind<IEngine>().To<Engine>();
Bind<Car>().ToSelf();

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

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

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

public class MyConfigModule : StandardModule
{
public override void Load()
{
Bind<Car>().ToSelf();
Bind<IEngine>().To<Engine>();
}
}

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

IKernel kernel = new StandardKernel(cfg1, cfg2, …);

Информации, приведенной выше, достаточно для начала работы с Ninject, но недостаточно для реализации более сложных сценариев. Такие возможности как контекстно-зависимое связывание, связывание, основанное на соглашениях, контроль времени жизни, существенно расширяют возможности применения Ninject и будут описаны в следующей статье.

Библиотека Code Contracts: контрактное программирование под .NET

.NET, C# No Comments

Code Contracts library for .NET 3.5: introduction in Russian. Code Contracts provide a language-agnostic way to express coding assumptions in .NET programs. The contracts take the form of pre-conditions, post-conditions, and object invariants and act as checked documentation of your external and internal APIs.

Контрактное программирование – это метод проектирования программ, предполагающий четкое специфицирование интерфейсов и описание обязанностей компонентов системы при взаимодействии друг с другом.

Обычно, контракт включает три основные группы элементов:

  • предусловия – обязательства, которые должны быть выполнены вызывающей стороной перед вызовом метода
  • постусловия – обязательства, гарантирующиеся вызываемым методом
  • инварианты класса – обязательства, что свойства класса будут удовлетворять определенным требованиям. Например, инвариантом для класса PositiveNumber, может быть утверждение, что хранимое число всегда больше или равно 0.

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

На PDC2008 было анонсировано появление в .NET 4.0 набора функций реализующих поддержку контрактного программирования и входящих в новое пространство имен System.Diagnostics.Contracts. На практике это будет выглядеть так:

class Account
{
   public double Balance { get; set; }

   public void Deposit(double amount)
   {
      Contract.Requires(amount >=0);

      Balance += amount;
   }

   public void Withdraw(double amount)
   {
      Contract.Requires(amount > 0);
      Contract.Requires(amount <= Balance);

      Balance -= amount;
   }

   public Account Transfer()
   {
      Contract.Ensures(Balance == 0);
      Contract.Ensures(Contract.Result<Account>() != null);

      Account newAccount = new Account() {Balance = this.Balance};
      Balance = 0;
      return newAccount;
   }

   [ContractInvariantMethod]
   protected void ValidAccount()
   {
      Contract.Invariant(Balance >=0);
   }
}

Как можно увидеть, контракт реализуется при помощи функций, что сразу вызывает вопрос, почему? Гораздо красивее была бы объявление контракта отдельно от кода, например с помощью атрибутов. К сожалению, атрибуты, по утверждению разработчиков, не позволяют описать все сценарии использования контрактов. Была возможность передавать в атрибуты сложные выражения, но они не захотели дублировать функциональность синтаксического анализатора. Так же они не захотели добавлять эту функциональность напрямую в язык, так как при этом другие .NET-языки не смогли бы ею воспользоваться. Так что мы имеем набор из десятка функций и нескольких атрибутов, которые войдут в mscorlib.

Code Contracts library

.NET 4 пока что не вышел, но попробовать контрактное программирование можно уже сейчас, загрузив библиотеку Code Contracts для .NET 3.5 с сайта Dev Labs. Библиотека доступна в двух вариантах: VSTS Edition для владельцев Visual Studio Team System, в которой доступны все возможности и ее применение разрешено в коммерческих целях, и Standard Edition для всех версий Visual Studio, за исключением Express Edition. Эта версия лицензируется для академических целей и не включает проверки контрактов во время компиляции.

При инсталляции, в систему добавляется библиотека Microsoft.Contracts, примеры и документация. Также, в Visual Studio инсталлятор добавляет в свойства проекта дополнительную страницу Code Contracts, на которой можно управлять поведением библиотеки. Для того, что бы начать работу с контрактами, в проект необходимо добавить ссылку на библиотеку Microsoft.Contracts.dll, расположенную в папке %PROGRAMFILES%/Microsoft/Contracts/PublicAssemblies.

Код, приведенный выше для примера, демонстрирует использование трех основных частей контракта: предусловий Contract.Requires в функциях Deposit и Withdraw, декларирующих, что они работают только с положительными суммами денег; постусловий Contract.Ensures в функции Transfer, которые заявляют, что в качестве результата функция должна вернуть новый счет и обнулить текущий; инварианта класса ValidAccount, утверждающего, что счет находиться в правильном состоянии, если его баланс больше или равен нулю.

Итак, контракт объявлен. Но что это нам дает? Что произойдет, если контракт будет нарушен, например, в случае такого вызова account.Deposit(-1000.0)? Это зависит от используемых настроек. Если включена статическая проверка контрактов, то уже на этапе компиляции будет выдано предупреждение о нарушении контракта. Если же включена только проверка контрактов во время выполнения, то, в простейшем случае, в момент вызова функции будет сгенерировано исключение ContractException. Если проверка отключена полностью, то никакой реакции не последует, так как в скомпилированном коде будут отсутствовать любые проверки.

  • На самом деле код не просто скомпилирован, но и пропущен через модуль ccrewrite.exe, который подвергает полученный в результате компиляции IL-код постобработке, в результате чего декларация контракта превращается в конкретные вызовы методов проверки.

Дополнительно, библиотека позволяет достаточно гибко менять параметры runtime-проверки контрактов. Во-первых, можно задать какие именно части контракта необходимо проверять, например, только предусловия. И, во-вторых, можно перехватить событие Contract.ContractFailed или задать свой класс, методы которого будут вызываться вместо генерации исключения. Таким образом, к примеру, можно автоматически протоколировать нарушения контракта перед завершением программы.

Далее коротко пройдемся по всем основным элементам контракта.

Предусловия

Декларируют условия, необходимые для работы метода, выражаются с помощью Contract.Requires() и обычно используются для проверки входящих параметров. Функция Requires()принимает в качестве параметра логическое выражение выражающее условие контракта. Так же существует перегруженная функция, принимающая в качестве второго параметра строку – сообщение, используемое при нарушении условия.

Contract.Requires(amount > 0, "Invalid amount");

Как уже упоминалось выше, в зависимости от настроек текущей конфигурации в скомпилированном коде проверок контракта может и не быть. Однако, если вы хотите, что бы проверка предусловия осуществлялась всегда, вне зависимости от настроек, то можно использовать функцию Contract.RequiresAlways(), которая будет работать даже при отключеннии проверок в настройках проекта.

Contract.RequiresAlways(x != 0);

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

if (условие) throw new...

то, добавив вызов Contract.EndContractBlock() после этих проверок, вы превратите их в предусловия. Аналогичное действие окажет и вызов Contract.Requires(). На выражения накладываются следующие ограничения – они не должны выполнять ничего кроме генерации исключения, оператор else не поддерживается.

Постусловия

Постусловия декларируют состояние, достигнутое после завершения работы метода. Простой пример постусловия:

Contract.Ensures(Balance == 0);

Постусловия, как и предусловия, объявляются в начале метода, однако в результате постобработки проверка условия будет осуществлена после выполнения всех инструкций метода. Это легко увидеть, запустив отладку метода содержащего постусловия – сначала отладчик пройдет по всем инструкциям, включая return и лишь затем перейдет к выполнению проверок.

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

Так, для верификации возвращаемого функцией значения используется специальный метод Contract.Result<T>(), где T – это тип возвращаемого функцией значения.

Contract.Ensures( Contract.Result<int>() > 0 );

Метод Contract.OldValue(e) позволяет получить значение переменной e до начала работы метода. Существует несколько ограничений на применение OldValue, главное из которых – переменная должна существовать перед началом выполнения инструкций метода и ее значение должно быть вычисляемым. В качестве таких переменных, например, могут выступать свойства класса или входящие переменные метода.

Contract.Ensures(Contract.OldValue(i) < n);

Для проверки в постусловиях значений out-переменных используется метод Contract.ValueAtReturn(out T t)

public void TestOut (out int a)
{
   Contract.Ensures(Contract.ValueAtReturn(out a) == 8);
   a = 8;
}

Инварианты класса

Инварианты класса относятся ко всему экземпляру класса и декларируют условия, при которых объект находиться в “хорошем” состоянии.

[ContractInvariantMethod]
protected void ValidAccount()
{
   Contract.Invariant(Balance >= 0);
}

Объявления всех инвариантов помещаются в один метод класса, помеченный атрибутом ContractInvariantMethod. Этот метод не должен содержать никакого кода, кроме объявлений инвариантов и не может возвращать значения. Каждый инвариант задается вызовом метода Contract.Invariant(). Проверка инвариантов класса происходит в конце каждого public метода класса – если посмотреть IL-код программы, то можно увидеть, что в процессе постобработки в конец каждого такого метода добавляется вызов нашей функции, помеченной атрибутом ContractInvariantMethod.

Дополнительно, в любом месте программы можно использовать Contract.Assert() для проверки условия в этой точке.

Contract.Assert( _number > 0 );

В процессе выполнения программы, если выражение ложно, то произойдет вызов Debug.Assert, в противном случае не последует никаких действий.

Контракты для интерфейсов

Описанные выше методы декларации контракта отлично работают в случае с конкретным классом. Однако что нам делать, если возникнет необходимость задать контракт для интерфейса? Ведь компилятор C# не позволит добавить для интерфейса тело функции с вызовом методов Contract.Requires() или Contract.Ensures(). На помощь приходит вспомогательный класс и пара атрибутов – ContractClass и ContractClassFor.

[ContractClass(typeof(SavingAccount))]
interface IAccount
{
   int Balance { get; }
   void Deposit(int amount);
}

[ContractClassFor(typeof(IAccount))]
class SavingAccount : IAccount
{
   int IAccount.Balance
   {
      get
      {
         Contract.Ensures(Contract.Result<int>() >=0 );
         return default(int);
      }
   }
   void IAccount.Deposit(int amount)
   {
      Contract.Requires(amount > 0);
   }
}

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

Поскольку статья задумывалась как введение в библиотеку Code Contracts, то я не стал описывать некоторые нюансы ее использования и методы, используемые в специальных случаях – с ними вы можете познакомиться в достаточно подробном документе, идущем вместе с библиотекой. Так же, для начала, может быть интересным видео с одним из разработчиков библиотеки, доступное на сайте Dev Labs.

Icons by N.Design Studio. Designed By Ben Swift.
Entries RSS Comments RSS Log in