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

191

При создании Default.aspx.cs в предыдущей статье мы воспользовались сокращением, которое выделено в примере ниже:

using System;
using System.Web.ModelBinding;
using TestAspNet45.Models;
using TestAspNet45.Models.Repository;
using TestAspNet45.Presenters;
using TestAspNet45.Presenters.Results;

namespace TestAspNet45.Pages
{
    public partial class Default : System.Web.UI.Page
    {
        public IPresenter<GuestResponse> Presenter { get; set; }

        protected void Page_Load(object sender, EventArgs e)
        {
            Presenter = new RSVPPresenter { repository = new MemoryRepository() };

            // ...
        }
    }
}

Несмотря на все усилия, затраченные на построение интерфейсов и их реализаций, мы создаем экземпляры классов RSVPPresenter и MemoryRepository напрямую. Одной из главных целей является обеспечение максимально возможно разделения компонентов, а из-за этого единственного оператора создается зависимость между представлением и классами презентатора и хранилища. Это значит, что в случае изменения реализации применяемых интерфейсов IPresenter или IRepository придется редактировать все файлы отделенного кода, находя и изменяя ссылки на классы.

Намного более эффективный подход предусматривает использование внедрения зависимостей (dependency injection - DI), которое также называется инверсией управления (inversion of control - IoC). Говоря простым языком, внедрение зависимостей позволяет хранить в приложении список интерфейсов и реализаций, которые должны применяться. Ответственность за создание экземпляров классов реализаций, когда они необходимы, берет на себя программный компонент под названием контейнер внедрения зависимостей (DI Container), что позволяет убрать ссылки на имена классов из классов представления и презентатора.

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

Доступно множество контейнеров внедрения зависимостей, но мы отдаем предпочтение Ninject. Настроить Ninject в приложении Web Forms проще всего с использованием диспетчера пакетов NuGet для установки пакета Ninject.Web, как показано на рисунке ниже. (Перед выбором пункта меню Project --> Manage NuGet Packages (Проект --> Управлять пакетами NuGet) удостоверьтесь, что в окне Solution Explorer выделен проект TestAspNet45, а не проект модульных тестов.)

Применение диспетчера пакетов NuGet для добавления пакета Ninject.Web в проект TestAspNet45

Диспетчер пакетов NuGet загрузит три пакета для установки запрошенного пакета - одна из важных характеристик диспетчера Nuget. Он управляет зависимостями, поэтому вам не придется самостоятельно отслеживать конкретные версии библиотек, которые должны использоваться.

После завершения процесса установки вы заметите добавление папки App_Start. Это стандартное место, куда помещается функциональность, требуемая при запуске приложения, и здесь будут находиться два новых файла классов: NinjectWeb.cs и NinjectWebCommon.cs.

Откройте файл NinjectWebCommon.cs и найдите метод RegisterServices(), который должен быть в начале файла. Добавьте в метод RegisterServices() код, приведенный в примере ниже:

// ...
private static void RegisterServices(IKernel kernel)
{
    DIConfiguration.SetupDI(kernel);
}
// ...

Мы предпочитаем держать конфигурационную информацию для внедрения зависимостей в отдельном файле класса, а оператор, добавленный в метод RegisterServices(), вызывает метод SetupDI класса по имени DIConfiguration. Этот класс определен в новом файле под названием \App_Start\DIConfiguration.cs, содержимое которого показано ниже:

using Ninject;
using TestAspNet45.Models;
using TestAspNet45.Models.Repository;
using TestAspNet45.Presenters;
using System.Collections.Generic;

namespace TestAspNet45.App_Start
{
    public static class DIConfiguration
    {

        public static void SetupDI(IKernel kernel)
        {
            kernel.Bind<IPresenter<GuestResponse>>().To<RSVPPresenter>();
            kernel.Bind<IRepository>().To<MemoryRepository>().InSingletonScope();
        }
    }
}

В этом классе понадобится настроить два отношения. Первое отношение сообщает Ninject о необходимости применения класса RSVPPresenter, когда поступает запрос интерфейса IPresenter<GuestResponse>:

kernel.Bind<IPresenter<GuestResponse>>().To<RSVPPresenter>();

В интерфейсе Ninject.IKernel определен строго типизированный метод Bind(), который вызывается для указания настраиваемого интерфейса. Возвращаемый методом Bind() объект позволяет ассоциировать класс RSVPPresenter как реализацию, подлежащую использованию. Применяя подобным образом методы Bind() и То(), мы указываем Ninject о том, что должен быть создан новый экземпляр класса RSVPPresenter для каждой получаемой реализации IPresenter<GuestResponse>.

Второе отношение несколько отличается:

kernel.Bind<IRepository>().To<MemoryRepository>().InSingletonScope();

Этот вызов метода InSingletonScope() сообщает Ninject о том, что на все запросы интерфейса IRepository необходимо отвечать единственным экземпляром класса MemoryRepository. Как вы помните, в рассмотренном ранее примере мы должны были создавать статические методы и переменные, чтобы все страницы могли разделять между собой единственный экземпляр хранилища. В новом приложении мы не хотим следовать такому подходу, т.к. это означает, что классам презентаторов должно быть известно имя используемой реализации интерфейса IRepository. Применяя Ninject, мы можем обеспечить разделение единственного экземпляра класса без помощи статических методов и переменных.

Конфигурирование контейнера Ninject

Контейнер Ninject конфигурирует себя автоматически так, что среда ASP.NET применяет его, когда требуются новые экземпляры классов для работы с веб-запросами. При создании нового экземпляра класса контейнер Ninject ищет атрибут Inject, который указывает ему на необходимость создания экземпляра одного из классов реализации и использования его для установки значения определенного свойства.

В примере ниже демонстрируется применение атрибута Inject к классу RSVPPresenter:

using System;
using TestAspNet45.Models;
using TestAspNet45.Models.Repository;
using TestAspNet45.Presenters.Results;

namespace TestAspNet45.Presenters
{
    public class RSVPPresenter : IPresenter<GuestResponse>
    {
        [Ninject.Inject]
        public IRepository repository { get; set; }

        IResult IPresenter<GuestResponse>.GetResult()
        {
            return new DataResult<GuestResponse>(new GuestResponse());
        }

        IResult IPresenter<GuestResponse>.GetResult(GuestResponse requestData)
        {
            repository.AddResponse(requestData);
            if (!requestData.WillAttend.HasValue)
                throw new System.ArgumentNullException("WillAttend равно null");
            if (requestData.WillAttend.Value)
                return new RedirectResult(@"/Content/seeyouthere.html");
            else
                return new RedirectResult(@"/Content/sorryyoucantcome.html");
        }
    }
}

После обнаружения атрибута Inject в классе RSVPPresenter контейнеру Ninject известно, что нам требуется создать экземпляр указанной реализации интерфейса IRepository и присвоить его свойству repository. Это позволяет указать, что нам необходим класс MemoryRepository, никак не уведомляя о его существовании класс RSVPPresenter. Если нужно изменить используемую реализацию хранилища, мы можем просто модифицировать конфигурацию в классе DIConfiguration, и это изменение окажет воздействие на все случаи применения атрибута Inject к свойствам типа IRepository в рамках приложения.

В примере ниже иллюстрируется применение атрибута Inject в файле Default.aspx.cs, что позволяет удалить оператор, который создавал экземпляры классов реализации с указанием их имен:

using System;
using System.Web.ModelBinding;
using TestAspNet45.Models;
using TestAspNet45.Models.Repository;
using TestAspNet45.Presenters;
using TestAspNet45.Presenters.Results;

namespace TestAspNet45.Pages
{
    public partial class Default : System.Web.UI.Page
    {
        [Ninject.Inject]
        public IPresenter<GuestResponse> Presenter { get; set; }

        protected void Page_Load(object sender, EventArgs e)
        {
            // Этот оператор был закомментирован
            // Presenter = new RSVPPresenter { repository = new MemoryRepository() };

            if (IsPostBack)
            {
                GuestResponse rsvp = 
                    ((DataResult<GuestResponse>)Presenter.GetResult()).DataItem;

                if (TryUpdateModel(rsvp, new FormValueProvider(ModelBindingExecutionContext)))
                {
                    Response.Redirect(((RedirectResult)Presenter.GetResult(rsvp)).Url);
                }
            }
        }
    }
}

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

Добавление итоговой страницы

Нам осталось завершить построение тестового приложения, добавив страницу итоговых сведений. Для этого мы добавили в папку Pages новую веб-форму по имени Summary.aspx. Контент веб-формы представлен в примере ниже:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Summary.aspx.cs" 
    Inherits="TestAspNet45.Pages.Summary" %>

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
    <title></title>
    <link rel="stylesheet" href="/Content/Styles.css" />
</head>
<body>
    <h2>Приглашения</h2>

    <h3>Люди которые были приглашены: </h3>
    <table>
        <thead>
            <tr>
                <th>Имя</th>
                <th>Email</th>
                <th>Телефон</th>
            </tr>
        </thead>
        <tbody><%= GetResponses(true) %></tbody>
    </table>
    <h3>Люди которые не придут: </h3>
    <table>
        <thead>
            <tr>
                <th>Имя</th>
                <th>Email</th>
                <th>Телефон</th>
            </tr>
        </thead>
        <tbody><%= GetResponses(false) %></tbody>
    </table>
</body>
</html>

Внутри фрагментов кода этой веб-формы вызывается метод GetResponses(), который был определен в файле отделенного кода, как показано в примере ниже:

using System;
using System.Text;
using System.Linq;
using System.Collections.Generic;
using TestAspNet45.Models;
using TestAspNet45.Presenters;
using TestAspNet45.Presenters.Results;

namespace TestAspNet45.Pages
{
    public partial class Summary : System.Web.UI.Page
    {
        private IEnumerable<GuestResponse> data;


        [Ninject.Inject]
        public IPresenter<IEnumerable<GuestResponse>> presenter { get; set; }

        protected void Page_Load(object sender, EventArgs e)
        {
            data = ((DataResult<IEnumerable<GuestResponse>>)presenter.GetResult()).DataItem;
        }

        protected string GetResponses(bool accepted)
        {
            StringBuilder html = new StringBuilder();
            var selectedData = data.Where(r => r.WillAttend.HasValue && r.WillAttend.Value == accepted);
            foreach (var rsvp in selectedData)
            {
                html.Append(String.Format("<tr><td>{0}</td><td>{1}</td><td>{2}</td>",
                    rsvp.Name, rsvp.Email, rsvp.Phone));
            }
            return html.ToString();
        }
    }
}

С помощью Ninject мы внедрили реализацию интерфейса IPresenter<IEnumerable<GuestResponse>>, что позволяет получать объекты данных из хранилища. Обратите внимание, что мы не получаем доступ к хранилищу напрямую, а всегда через презентатор, даже для простых операций. Это обеспечивает целостность реализации MVP и означает возможность простого добавления бизнес-логики в класс презентатора, если она понадобится в будущем.

Создание презентатора

Мы собираемся повторно использовать класс RSVPPresenter, поэтому нам нужен единственный презентатор, который имеет дело со всеми объектами GuestResponse. В примере ниже видно, что в классе RSVPPresenter был явно реализован интерфейс IPresenter<IEnumerable<GuestResponse>>:

using System;
using TestAspNet45.Models;
using TestAspNet45.Models.Repository;
using TestAspNet45.Presenters.Results;
using System.Collections.Generic;

namespace TestAspNet45.Presenters
{
    public class RSVPPresenter : IPresenter<GuestResponse>, 
        IPresenter<IEnumerable<GuestResponse>>
    {

        [Ninject.Inject]
        public IRepository repository { get; set; }

        IResult IPresenter<GuestResponse>.GetResult()
        {
            return new DataResult<GuestResponse>(new GuestResponse());
        }

        IResult IPresenter<GuestResponse>.GetResult(GuestResponse requestData)
        {
            // ...
        }

        IResult IPresenter<IEnumerable<GuestResponse>>.GetResult()
        {
            return new DataResult<IEnumerable<GuestResponse>>(repository.GetAllResponses());
        }

        IResult IPresenter<IEnumerable<GuestResponse>>.GetResult
             (IEnumerable<GuestResponse> requestData)
        {
            throw new System.NotImplementedException();
        }
    }
}

Мы должны реализовать только метод GetResult(), не принимающий аргументов, поскольку никакие данные с помощью веб-формы Summary.aspx из браузера не отправляются. Мы просто применяем хранилище для извлечения всех доступных объектов данных и возвращаем их представлению посредством объекта DataResult.

Конфигурирование внедрения зависимостей

Последний шаг связан с добавлением в метод SetupDI() класса DIConfiguration внутри папки App_Start записи, которая сообщит Ninject о том, что для обслуживания запросов интерфейса IPresenter<IEnumerable<GuestResponse>> должны создаваться экземпляры класса RSVPPresenter:

using Ninject;
using TestAspNet45.Models;
using TestAspNet45.Models.Repository;
using TestAspNet45.Presenters;
using System.Collections.Generic;

namespace TestAspNet45.App_Start
{
    public static class DIConfiguration
    {

        public static void SetupDI(IKernel kernel)
        {
            kernel.Bind<IPresenter<GuestResponse>>().To<RSVPPresenter>();
            kernel.Bind<IPresenter<IEnumerable<GuestResponse>>>().To<RSVPPresenter>();
            kernel.Bind<IRepository>().To<MemoryRepository>().InSingletonScope();
        }
    }
}

На этом переделка приложения TestAspNet45 с применением шаблона MVP завершена.

Ищите свободу от табачного дыма, хотите бросить курить? Выберите на Dekang электронные сигареты и жидкости для электронных сигарет. Бросаем курить вместе!

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