Внедрение зависимостей Ninject

146

Как было отмечено в статье "Архитектура ASP.NET MVC 5", предпочтительным контейнером DI является Ninject. Он прост, элегантен и легок в применении. Существуют более сложные альтернативы, но Ninject требует для своего функционирования минимальной конфигурации. Если вам не нравится Ninject, рекомендуем попробовать Unity - альтернативную реализацию от Microsoft.

Мы собираемся начать с создания проекта для простого примера по имени EssentialTools, который будет использоваться далее. При этом применяется шаблон ASP.NET MVC Web Application (Веб-приложение ASP.NET MVC) с вариантом Empty и отмечен флажок MVC в разделе Add folders and core references for с целью добавления базового содержимого проекта MVC.

Добавьте в папку Models проекта файл класса по имени Product.cs и приведите его содержимое в соответствие с кодом ниже. Это тот же самый класс модели, который использовался в предыдущих статьях, с единственным изменением - пространство имен относится к проекту EssentialTools:

namespace EssentialTools.Models
{
    public class Product
    {
        public int ProductID { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public decimal Price { get; set; }
        public string Category { set; get; }
    }
}

Также необходимо добавить класс, который будет вычислять общую сумму для коллекции объектов Product. Добавьте в папку Models новый файл класса по имени LinqValueCalculator.cs с содержимым, показанным в примере ниже:

using System.Collections.Generic;
using System.Linq;

namespace EssentialTools.Models
{
    public class LinqValueCalculator
    {
        public decimal ValueProducts(IEnumerable<Product> products)
        {
            return products.Sum(p => p.Price);
        }
    }
}

В классе LinqValueCalculator определен единственный метод ValueProducts(), который с помощью LINQ-метода Sum() получает сумму значений свойства Price всех объектов Product в перечислении products, передаваемом методу.

Наконец, последний класс модели называется ShoppingCart и представляет коллекцию объектов Product, он использует LinqValueCalculator для вычисления общей суммы. Создайте новый файл класса по имени ShoppingCart.cs и приведите его содержимое в соответствие примером ниже:

using System.Collections.Generic;

namespace EssentialTools.Models
{
    public class ShoppingCart
    {
        private LinqValueCalculator calc;

        public ShoppingCart(LinqValueCalculator calcParam)
        {
            calc = calcParam;
        }

        public IEnumerable<Product> Products { get; set; }

        public decimal CalculateProductTotal()
        {
            return calc.ValueProducts(Products);
        }
    }
}

Добавьте в папку Controllers новый контроллер по имени HomeController с содержимым, приведенным в примере ниже. Метод действия Index() создает массив объектов Product и с помощью объекта LinqValueCalculator вычисляет общую сумму, которую затем передает методу View(). При вызове метода View() представление не указывается, поэтому будет использоваться стандартное представление, ассоциированное с методом действия (файл Views/Home/Index.cshtml).

using System.Web.Mvc;
using EssentialTools.Models;

namespace EssentialTools.Controllers
{
    public class HomeController : Controller
    {
        private Product[] products = {
            new Product {Name = "Каяк", Category = "Водные виды спорта", Price = 275M},
            new Product {Name = "Спасательный жилет", Category = "Водные виды спорта", Price = 48.95M},
            new Product {Name = "Мяч", Category = "Футбол", Price = 19.50M},
            new Product {Name = "Угловой флажок", Category = "Футбол", Price = 34.95M}
        };

        public ActionResult Index()
        {
            LinqValueCalculator calc = new LinqValueCalculator();
            ShoppingCart cart = new ShoppingCart(calc) { 
                Products = products 
            };
            decimal totalValue = cart.CalculateProductTotal();
            return View(totalValue);
        }
    }
}

Последним в проект добавляется представление по имени Index. При создании представления не играет роли, какие флажки отмечены, поскольку содержимое будет совпадать с показанным в примере ниже:

@model decimal

@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Значение</title>
</head>
<body>
    <div>
        Общая стоимость $@Model
    </div>
</body>
</html>

В этом представлении применяется выражение @Model для отображения значения decimal, переданного из метода действия. Запустив проект, вы увидите значение общей суммы, вычисленное классом LinqValueCalculator. Несмотря на простоту, проект обозначает место различных инструментов и приемов, которые будут описаны в этой и последующих статьях.

Проверка примера приложения

Идея внедрения зависимостей (DI) была представлена ранее. Напомним, что она состоит в разъединении компонентов приложения MVC с помощью комбинации интерфейсов и контейнера DI, создающего экземпляры классов путем создания реализаций интерфейсов, от которых они зависят, и их внедрения внутрь конструктора.

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

Сущность проблемы

В примере приложения присутствует базовая проблема, которую решает внедрение зависимостей: тесно связанные классы. Класс ShoppingCart тесно связан с классом LinqValueCalculator, а класс HomeController тесно связан сразу с двумя классами - ShoppingCart и LinqValueCalculator.

Это означает, что в случае замены класса LinqValueCalculator придется искать все ссылки на него в классах, которые тесно с ним связаны. В таком простом проекте это не составит труда, однако в реальных проектах данная задача становится утомительной и чреватой ошибками, особенно если речь идет о разных реализациях калькулятора (например, в целях тестирования), а не об обычной замене одного класса другим.

Применение интерфейса

Часть проблемы можно решить используя интерфейс C# для абстрагирования определения функциональности калькулятора от его реализации. Чтобы продемонстрировать это, добавим в папку Models файл IValueCalculator.cs и создадим интерфейс, показанный в примере ниже:

using System.Collections.Generic;

namespace EssentialTools.Models
{
    public interface IValueCalculator
    {
        decimal ValueProducts(IEnumerable<Product> products);
    }
}

Интерфейс позволяет устранить тесную связь между классами ShoppingCart и LinqValueCalculator, что можно видеть в примере ниже:

using System.Collections.Generic;

namespace EssentialTools.Models
{
    public class ShoppingCart
    {
        private IValueCalculator calc;

        public ShoppingCart(IValueCalculator calcParam)
        {
            calc = calcParam;
        }

        public IEnumerable<Product> Products { get; set; }

        public decimal CalculateProductTotal()
        {
            return calc.ValueProducts(Products);
        }
    }
}

Мы достигли определенного прогресса, но компилятор C# требует указания класса для реализации во время создания экземпляра интерфейса, что вполне понятно, поскольку ему необходимо знать, какая реализация должна использоваться. В контроллере Home по-прежнему остается проблема при создании объекта LinqValueCalculator:

// ...
public ActionResult Index()
{
    IValueCalculator calc = new LinqValueCalculator();
    ShoppingCart cart = new ShoppingCart(calc) { 
        Products = products 
    };
    decimal totalValue = cart.CalculateProductTotal();
    return View(totalValue);
}
//...

Цель, которую мы планируем добиться с помощью Ninject, заключается в том, чтобы добраться до точки, где бы сообщалось о необходимости создать экземпляр реализации интерфейса IValueCalculator, но детали, реализация которых требуется, не являлись бы частью кода контроллера Home.

Это будет означать указание инструменту Ninject о том, что LinqValueCalculator является реализацией интерфейса IValueCalculator, которая должна применяться, и модификацию класса HomeController так, чтобы он получал свои объекты через Ninject, а не за счет использования ключевого слова new.

Добавление Ninject в проект Visual Studio

Простейший способ добавления Ninject в проект MVC предусматривает применение интегрированной в Visual Studio поддержки инструмента NuGet, который облегчает установку широкого спектра пакетов и сохраняет их в актуальном состоянии. Средство NuGet использовалось в статье "Простое приложение ASP.NET MVC 5" для установки библиотеки Bootstrap, однако доступен огромный каталог пакетов, в том числе Ninject.

Выберите пункт меню Tools --> Library Package Manager --> Package Manager Console (Сервис --> Диспетчер библиотечных пакетов --> Консоль диспетчера пакетов), чтобы открыть окно командной строки NuGet. Введите следующие команды:

Install-Package Ninject -version 3.0.1.10
Install-Package Ninject.Web.Common -version 3.0.0.7
Install-Package Ninject.MVC3 -Version 3.0.0.6

Первая команда устанавливает основной пакет Ninject, а другие команды - расширения ядра, которые обеспечивают эффективное взаимодействие Ninject с приложениями ASP.NET (как вскоре будет описано). Не обращайте внимания на ссылку MVC3 в имени последнего пакета - он в равной степени хорошо работает и с версией MVC 5.

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

Начало работы с Ninject

Обеспечение работы базовой функциональности Ninject осуществляется в три этапа, которые показаны в примере ниже:

using System.Web.Mvc;
using EssentialTools.Models;
using Ninject;

namespace EssentialTools.Controllers
{
    public class HomeController : Controller
    {
        private Product[] products = {
            new Product {Name = "Каяк", Category = "Водные виды спорта", Price = 275M},
            new Product {Name = "Спасательный жилет", Category = "Водные виды спорта", Price = 48.95M},
            new Product {Name = "Мяч", Category = "Футбол", Price = 19.50M},
            new Product {Name = "Угловой флажок", Category = "Футбол", Price = 34.95M}
        };

        public ActionResult Index()
        {
            IKernel ninjectKernel = new StandardKernel();
            ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>();

            IValueCalculator calc = ninjectKernel.Get<IValueCalculator>();

            ShoppingCart cart = new ShoppingCart(calc) { 
                Products = products 
            };
            decimal totalValue = cart.CalculateProductTotal();
            return View(totalValue);
        }
    }
}

Первый этап заключается в подготовке Ninject к использованию. Для этого создается экземпляр ядра Ninject, который представляет собой объект, ответственный за распознавание зависимостей и создание новых объектов. Когда возникает потребность в каком-либо объекте, вместо применения ключевого слова new производится обращение к ядру. Вот оператор, создающий ядро:

IKernel ninjectKernel = new StandardKernel();

Необходимо создать реализацию интерфейса Ninject.IKernel, что делается конструированием нового экземпляра класса StandardKernel. Библиотека Ninject может быть расширена и настроена для работы с различными видами ядра. (На самом деле, вы можете годами пользоваться Ninject и иметь дело только со StandardKernel.)

Второй этап процесса состоит в конфигурировании ядра Ninject с целью предоставления сведений о том, какие объекты реализации должны применяться для каждого интерфейса, с которым придется работать. Ниже показан оператор из примера выше, который делает это:

ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>();

Для создания отношения библиотека Ninject использует параметры типов С#: мы указываем интерфейс, с которым необходимо работать, в параметре типа для метода Bind() и вызываем метод То() на результате, который он возвращает. Класс реализации, экземпляр которого нужно создать, передается методу То() в виде параметра типа. Данный оператор сообщает Ninject о том, что зависимости интерфейса IValueCalculator должны быть распознаны путем создания экземпляра класса LinqValueCalculator.

Последний этап — это действительное использование Ninject, что делается посредством метода Get() ядра, как показано ниже:

IValueCalculator calc = ninjectKernel.Get<IValueCalculator>();

Параметр типа, применяемый для метода Get(), сообщает Ninject интересующий интерфейс, а возвращаемый этим методом результат представляет собой экземпляр типа реализации, который ранее был указан в методе То().

Настройка внедрения зависимостей MVC

В результате выполнения трех шагов, описанных в предыдущем разделе, в Ninject настраивается информация о том, экземпляр какого класса реализации должен быть создан для удовлетворения запросов интерфейса IValueCalculator. Разумеется, мы никак не улучшили приложение, т.к. эта информация все еще определена в контроллере Home, а это значит, что контроллер Home по-прежнему тесно связан с классом LinqValueCalculator.

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

Создание распознавателя зависимостей

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

Для настройки распознавателя создайте новую папку по имени Infrastructure, куда будут помещаться классы, не вписывающиеся в другие папки внутри приложения MVC. Добавьте новый файл класса под названием NinjectDependencyResolver.cs, содержимое которого показано в примере ниже:

using System;
using System.Collections.Generic;
using System.Web.Mvc;
using EssentialTools.Models;
using Ninject;
using Ninject.Web.Common;

namespace EssentialTools.Infrastructure
{
    public class NinjectDependencyResolver : IDependencyResolver
    {
        private IKernel kernel;

        public NinjectDependencyResolver(IKernel kernelParam)
        {
            kernel = kernelParam;
            AddBindings();
        }

        public object GetService(Type serviceType)
        {
            return kernel.TryGet(serviceType);
        }

        public IEnumerable<object> GetServices(Type serviceType)
        {
            return kernel.GetAll(serviceType);
        }

        private void AddBindings()
        {
            kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
        }
    }
}

Класс NinjectDependencyResolver реализует интерфейс IDependencyResolver который принадлежит пространству имен System.Web.Mvc и применяется MVC Framework для получения необходимых объектов. Инфраструктура MVC Framework будет вызывать метод GetService() или GetServices(), когда ей понадобится экземпляр класса для обслуживания входящего запроса.

Работа распознавателя зависимостей заключается в создании этого экземпляра - задача, которая решается вызовом методов TryGet() и GetAll() из Ninject. Метод TryGet() функционирует подобно используемому ранее методу Get(), но вместо генерации исключения он возвращает null, когда нет подходящей привязки. Метод GetAll() поддерживает множество привязок для одного типа, которые применяются при наличии нескольких доступных и отличающихся объектов реализации.

В классе распознавателя зависимостей также настраивается привязка Ninject. В методе AddBindings() используются методы Bind() и To() для установки отношения между интерфейсом IValueCalculator и классом LinqValueCalculator.

Регистрация распознавателя зависимостей

Простого создания реализации интерфейса IDependencyResolver недостаточно - инфраструктуре MVC Framework необходимо также сообщить на необходимость его использования.

Пакеты Ninject, добавленные с помощью NuGet, создали в папке App_Start файл по имени NinjectWebCommon.cs, в котором определены методы, вызываемые автоматически при запуске приложения; целью является интеграция в жизненный цикл запросов ASP.NET.

В метод RegisterServices() класса NinjectWebCommon добавляется оператор, который создает экземпляр класса NinjectDependencyResolver и вызывает статический метод SetResolver(), определенный классом System.Web.Mvc.DependencyResolver, для регистрации распознавателя в инфраструктуре MVC Framework, как показано в примере ниже. Не беспокойтесь, если это выглядит для вас непонятным. Результатом данного оператора является создание шлюза между Ninject и поддержкой внедрения зависимостей в MVC Framework.

// ...
private static void RegisterServices(IKernel kernel)
{
    System.Web.Mvc.DependencyResolver.SetResolver(new
        EssentialTools.Infrastructure.NinjectDependencyResolver(kernel));
}      
// ...

Рефакторинг контроллера Home

Финальный шаг заключается в проведении рефакторинга контроллера Home, чтобы он мог пользоваться средствами, настроенными в предшествующих разделах. Внесенные изменения приведены в примере ниже:

using System.Web.Mvc;
using EssentialTools.Models;
using Ninject;

namespace EssentialTools.Controllers
{
    public class HomeController : Controller
    {
        private IValueCalculator calc;

        private Product[] products = {
            new Product {Name = "Каяк", Category = "Водные виды спорта", Price = 275M},
            new Product {Name = "Спасательный жилет", Category = "Водные виды спорта", Price = 48.95M},
            new Product {Name = "Мяч", Category = "Футбол", Price = 19.50M},
            new Product {Name = "Угловой флажок", Category = "Футбол", Price = 34.95M}
        };

        public HomeController(IValueCalculator calcParam)
        {
            calc = calcParam;
        }

        public ActionResult Index()
        {
            ShoppingCart cart = new ShoppingCart(calc) { 
                Products = products 
            };
            decimal totalValue = cart.CalculateProductTotal();
            return View(totalValue);
        }
    }
}

Основным изменением является добавление конструктора класса, который принимает реализацию интерфейса IValueCalculator, модифицируя класс HomeController так, что он объявляет зависимость. Библиотека Ninject предоставит объект, который реализует интерфейс IValueCalculator, когда создается экземпляр контроллера, с использованием конфигурации, созданной в классе NinjectDependencyResolver.

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

Запустив пример, вы увидите результат, аналогичный показанному на рисунке выше. Разумеется, точно такой же результат получался и при создании экземпляра класса LinqValueCalculator напрямую в контроллере. То, что было создано - это пример внедрения через конструктор, которое представляет собой одну из форм внедрения зависимостей. Ниже описано, что происходит, когда приложение запущено, а в Google Chrome запрашивается корневой URL приложения:

  1. Инфраструктура MVC Framework получает запрос и выясняет, что он предназначен для контроллера Home.

  2. Инфраструктура MVC Framework запрашивает у специального распознавателя зависимостей создание нового экземпляра класса HomeController, указывая этот класс в параметре Type метода GetService().

  3. Распознаватель зависимостей запрашивает у Ninject создание нового экземпляра класса HomeController, передавая объект Type методу TryGet.

  4. Библиотека Ninject проверяет конструктор HomeController и обнаруживает, что он объявляет зависимость от интерфейса IValueCalculator, для которой предусмотрена привязка.

  5. Библиотека Ninject создает экземпляр класса LinqValueCalculator и применяет его для создания нового экземпляра класса HomeController.

  6. Библиотека Ninject передает экземпляр HomeController специальному распознавателю зависимостей, который возвращает его MVC Framework. Инфраструктура MVC Framework использует экземпляр контроллера для обслуживания запроса.

Мы привели довольно подробные объяснения, поскольку поначалу DI может вызывать некоторую путаницу во время применения. С выбранным здесь подходом связано одно преимущество: любой контроллер в приложении может объявить зависимость, и MVC Framework будет использовать Ninject для ее распознавания.

Лучше всего то, что если нужно заменить LinqValueCalculator другой реализацией, то потребуется модифицировать только класс распознавателя зависимостей, т.к. это единственное место, где должна быть указана реализация, применяемая для удовлетворения зависимостей от интерфейса IValueCalculator.

Создание цепочек зависимостей

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

Для демонстрации этой функциональной возможности мы добавили в папку Models проекта файл по имени Discount.cs и определили в нем новый интерфейс, а также реализующий его класс:

namespace EssentialTools.Models
{
    public interface IDiscountHelper
    {
        decimal ApplyDiscount(decimal totalParam);
    }

    public class DefaultDiscountHelper : IDiscountHelper
    {
        public decimal ApplyDiscount(decimal totalParam)
        {
            return (totalParam - (10m / 100m * totalParam));
        }
    }
}

В интерфейсе IDiscountHelper определен метод ApplyDiscount(), который применяет скидку к значению decimal. Класс DefaultDiscountHelper реализует интерфейс IDiscountHelper и применяет фиксированную 10-процентную скидку. Класс LinqValueCalculator модифицирован, чтобы при выполнении вычислений он использовал интерфейс IDiscountHelper:

using System.Collections.Generic;
using System.Linq;

namespace EssentialTools.Models
{
    public class LinqValueCalculator : IValueCalculator
    {
        private IDiscountHelper discounter;

        public LinqValueCalculator(IDiscountHelper discountParam)
        {
            discounter = discountParam;
        }

        public decimal ValueProducts(IEnumerable<Product> products)
        {
            return discounter.ApplyDiscount(products.Sum(p => p.Price));
        }
    }
}

Новый конструктор класса объявляет зависимость от интерфейса IDiscountHelper. Получаемый конструктором объект реализации присваивается полю, которое используется в методе ValueProducts для применения скидки к суммарному значению объектов Product.

Интерфейс IDiscountHelper привязывается к классу реализации посредством ядра Ninject в классе NinjectDependencyResolver, как это делалось для IValueCalculator:

// ...
private void AddBindings()
{
    kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
    kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>();
}
// ...

Мы создали цепочку зависимостей. Контроллер Home зависит от интерфейса IValueCalculator, который библиотека Ninject должна распознавать с использованием класса LinqValueCalculator. Класс LinqValueCalculator зависит от интерфейса IDiscountHelper, который библиотека Ninject должна распознавать с применением класса DefaultDiscountHelper.

Библиотека Ninject гладко распознает цепочку зависимостей, создавая объекты, которые необходимы для распознавания каждой зависимости, и в конечном итоге в данном примере создает экземпляр класса HomeController с целью обслуживания HTTP-запроса.

Указание значений для свойств и параметров конструктора

Создаваемые Ninject объекты можно конфигурировать, предоставляя значения для свойств во время привязки интерфейса к его реализации. Для демонстрации этой возможности мы изменили класс DefaultDiscountHelper, включив в него свойство DiscountSize для подсчета размера скидки, как показано в примере ниже:

namespace EssentialTools.Models
{
    public interface IDiscountHelper
    {
        decimal ApplyDiscount(decimal totalParam);
    }

    public class DefaultDiscountHelper : IDiscountHelper
    {
        public decimal DiscountSize { get; set; }

        public decimal ApplyDiscount(decimal totalParam)
        {
            return (totalParam - (DiscountSize / 100m * totalParam));
        }
    }
}

Когда ядру Ninject сообщается класс, который должен использоваться для интерфейса, с помощью метода WithPropertyValue() можно установить значение свойства DiscountSize в классе DefaultDiscountHelper. В примере ниже приведено соответствующее изменение, внесенное в метод AddBindings() класса NinjectDependencyResolver. Обратите внимание, что имя устанавливаемого свойства указывается в виде строки:

//...
private void AddBindings()
{
    kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
    kernel.Bind<IDiscountHelper>()
        .To<DefaultDiscountHelper>().WithPropertyValue("DiscountSize", 50M);
}
//...

Изменять какие-то другие привязки либо способ применения метода Get() для получения экземпляра класса ShoppingCart не понадобится. Значение свойства устанавливается вслед за созданием экземпляра класса DefaultDiscountHelper и обеспечивает деление суммарного значения элементов пополам. Результат внесенного изменения показан на рисунке ниже:

Результат применения скидки с помощью свойства во время распознавания цепочки зависимостей

Если требуется установить значение более одного свойства, для охвата их всех вызовы метода WithPropertyValue() можно выстраивать в цепочку. Аналогично можно поступать с параметрами конструктора. В примере ниже представлен переделанный код класса DefaultDiscountHelper, в котором размер скидки передается в параметре конструктора:

namespace EssentialTools.Models
{
    public interface IDiscountHelper
    {
        decimal ApplyDiscount(decimal totalParam);
    }

    public class DefaultDiscountHelper : IDiscountHelper
    {
        public decimal discountSize;

        public DefaultDiscountHelper(decimal discountParam)
        {
            discountSize = discountParam;
        }

        public decimal ApplyDiscount(decimal totalParam)
        {
            return (totalParam - (discountSize / 100m * totalParam));
        }
    }
}

Чтобы привязать этот класс с помощью Ninject, мы указываем значение параметра конструктора с помощью метода WithConstructorArgument() внутри метода AddBindings:

// ...
private void AddBindings()
{
    kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
    kernel.Bind<IDiscountHelper>()
       .To<DefaultDiscountHelper>().WithConstructorArgument("discountParam", 50M);
}
// ...

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

Обратите внимание, что мы не просто заменили вызов WithPropertyValue() вызовом WithConstructorArgument(). Было также изменено имя целевого члена, чтобы оно соответствовало соглашению, принятому в C# для имен параметров.

Использование условной привязки

Библиотека Ninject поддерживает набор методов условной привязки, позволяющих указывать классы, которые ядро должно использовать в ответ на запросы определенных интерфейсов. Для демонстрации этого средства мы добавили в папку Models примера проекта новый файл по имени FlexibleDiscountHelper.cs с содержимым, приведенным в примере ниже:

namespace EssentialTools.Models
{
    public class FlexibleDiscountHelper : IDiscountHelper
    {
        public decimal ApplyDiscount(decimal totalParam)
        {
            decimal discount = totalParam > 100 ? 70 : 25;
            return (totalParam - (discount / 100m * totalParam));
        }
    }
}

Класс FlexibleDiscountHelper применяет разные скидки на основе величины обшей суммы. Теперь, когда доступен выбор из классов, реализующих интерфейс IDiscountHelper, можно изменить метод AddBindigs() класса NinjectDependencyResolver, чтобы сообщить Ninject, когда необходимо использовать каждый из них, как показано в примере ниже:

// ...
private void AddBindings()
{
    kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
    kernel.Bind<IDiscountHelper>()
      .To<DefaultDiscountHelper>().WithConstructorArgument("discountParam", 50M);
    kernel.Bind<IDiscountHelper>().To<FlexibleDiscountHelper>()
      .WhenInjectedInto<LinqValueCalculator>();
}
// ...

Новая привязка указывает, что ядро Ninject должно применять класс FlexibleDiscountHelper в качестве реализации интерфейса IDiscountHelper, когда оно создает объект LinqValueCalculator. Обратите внимание, что первоначальная привязка для IDiscountHelper осталась на месте. Ядро Ninject пытается найти наилучшее соответствие, однако полезно предусмотреть для того же самого класса или интерфейса стандартную привязку, которая будет служить запасным вариантом на случай, если условная привязка не может быть удовлетворена.

В библиотеке Ninject поддерживается несколько методов условной привязки, наиболее полезные из которых перечислены в таблице ниже:

Методы условной привязки Ninject
Метод Описание
When(predicate)

Привязка используется, когда вычисленное значение предиката (predicate), представленного с помощью лямбда-выражения, равно true

WhenClassHas<T>()

Привязка используется, когда внедряемый класс аннотирован атрибутом, типом которого является T

WhenInjectedInto(T)

Привязка используется, когда типом внедряемого класса является T

Установка области действия объектов

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

Для демонстрации происходящего конструктор класса LinqValueCalculator изменен так, чтобы при создании нового экземпляра выводить сообщение в окно Output среды Visual Studio:

using System.Collections.Generic;
using System.Linq;

namespace EssentialTools.Models
{
    public class LinqValueCalculator : IValueCalculator
    {
        private IDiscountHelper discounter;
        private static int counter = 0;

        public LinqValueCalculator(IDiscountHelper discountParam)
        {
            discounter = discountParam;
            System.Diagnostics.Debug.WriteLine(
                string.Format("Экземпляр класса LinqValueCalculator №{0} создан", ++counter));
        }

        public decimal ValueProducts(IEnumerable<Product> products)
        {
            return discounter.ApplyDiscount(products.Sum(p => p.Price));
        }
    }
}

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

В примере ниже контроллер Home модифицирован и теперь требует у Ninject двух реализаций интерфейса IValueCalculator в конструкторе:

// ...
public HomeController(IValueCalculator calcParam, IValueCalculator calc2)
{
    calc = calcParam;
}
// ...

Никаких полезных действий с объектом, предоставляемым ядром Ninject, не выполняется - важен лишь сам факт запрашивания двух реализаций интерфейса. Запустив пример, вы увидите в окне Output среды Visual Studio сообщения, которые указывают на то, что ядро Ninject создало два экземпляра класса LinqValueCalculator:

Экземпляр класса LinqValueCalculator №1 создан
Экземпляр класса LinqValueCalculator №2 создан

Многократное создание экземпляров класса LinqValueCalculator не вызывает проблем, но это нельзя утверждать применительно ко всем классам. В определенных классах понадобится разделять единственный экземпляр по всему приложению, а в других - создавать новый экземпляр для каждого HTTP-запроса, который получает платформа ASP.NET.

Библиотека Ninject позволяет управлять жизненным циклом создаваемых объектов, используя средство под названием области действия, которое выражается вызовом метода при настройке привязки между интерфейсом и типом его реализации. В примере ниже демонстрируется применение наиболее удобной области действия для приложений MVC Framework - области действия запроса - к классу LinqValueCalculator в файле NinjectDependencyResolver.cs:

private void AddBindings()
{
    kernel.Bind<IValueCalculator>().To<LinqValueCalculator>().InRequestScope();
    kernel.Bind<IDiscountHelper>()
      .To<DefaultDiscountHelper>().WithConstructorArgument("discountParam", 50M);
    kernel.Bind<IDiscountHelper>().To<FlexibleDiscountHelper>()
      .WhenInjectedInto<LinqValueCalculator>();
}

Расширяющий метод InRequestScope(), определенный в пространстве имен Ninject.Web.Common, сообщает Ninject о том, что для каждого HTTP-запроса, получаемого ASP.NET, должен создаваться только один экземпляр класса LinqValueCalculator. Каждый запрос получит собственный отдельный объект; но множество зависимостей будут распознаваться в рамках одного запроса с применением единственного экземпляра указанного класса.

Чтобы увидеть результат этого изменения, необходимо запустить приложение и заглянуть в окно Output среды Visual Studio, в котором будет отражено сообщение о том, что ядро Ninject создало только один экземпляр класса LinqValueCalculator. Если обновить окно браузера без перезапуска приложения, будет видно, что ядро Ninject создало второй объект.

Библиотека Ninject поддерживает несколько областей действия объектов, наиболее полезные из которых описаны в таблице ниже:

Методы областей действия в Ninject
Метод Описание
InTransientScope()

Аналогичен отсутствию указания области действия и создает новый объект для каждой распознаваемой зависимости

InSingletonScope()

Создает одиночный экземпляр, который разделяется по всему приложению. Ядро Ninject будет создавать экземпляр, если используется метод InSingletonScope(), или же экземпляр можно предоставить посредством метода ToConstant()

InThreadScope()

Создает одиночный экземпляр, который используется для распознавания зависимостей объектов, запрашиваемых в одном потоке

InRequestScope()

Создает одиночный экземпляр, который используется для распознавания зависимостей объектов, запрашиваемых в одном HTTP-запросе

Пройди тесты
Лучший чат для C# программистов