Monday, December 16, 2013

Использование Ninject

Данная статья посвящена Inversion of  Control (IoC) контейнеру Ninject. Данный IoC контейнер имеет легковесный api, который позволяет в кратчайшие строки освоить работу с ним. Ninject так же прост в изучении, как и Autofac. А благодаря множеству отдельных библиотек использование Ninject предоставляет все доступные на сегодняшний день возможности для управления зависимостями. В своей основе Ninject очень прост для того, чтобы, потратив минимальный отрезок времени, его можно было использовать в Enterprise decision (бизнес-решениях). Хотя он не поддерживает interception, конфигурирования через xml (для этого нужно скачивать отдельно библиотеки, которых на момент написания статьи было около 30), asp.new mvc, MVC3, NLog, Glimpse и т.д. Чтобы продемонстрировать, как использовать данный IoC контейнер, рассмотрим ту же реализацию электронной библиотеки, которая была использована для рассмотрения предыдущих IoC контейнеров в предыдущих статьях (Unity, Autofac, CastleWindsor и StructureMap). Диаграмма классов приведена ниже.
Для того, чтобы показать, как использовать контейнер Ninject, удалим в App.xaml строчку для запуска формы в Dependency Property (DP) - StartupUri. Для инициализации IoC контейнера перед стартом программы необходимо перейти в классы App.xaml.cs и переопределить метод OnStartup. Затем необходимо написать соответствующую реализацию.
protected override void OnStartup(StartupEventArgs e)
{
    var kernel = new StandardKernel();
    kernel.Bind<IBook>().To<LibraryBook>();
    kernel.Bind<ILibraryBookService>().To<LibraryBookService>();
    kernel.Bind<IVisitorRepository>().To<VisitorRepository>();
    kernel.Bind<MainViewModel>().ToSelf();
    kernel.Bind<MainWindow>().ToSelf();
    var model = kernel.Get<MainViewModel>();
    var view = kernel.Get<MainWindow>();
    view.DataContext = model;
    view.Show();
}
Для простых программ достаточно всего две инструкции:
Bind().To();
Bind().ToSelf();
Но для более сложных можно использовать дополнительные методы по выбору lifetime manager (менеджер жизни объектов), инициализацию и использование конструктора на выбор, method injection (MI) и property injection (PI) и многое другое. Ninject также хорош тем, что он позволяет выполнять конфигурации в отдельных модулях. Посмотрим, насколько упростится наш пример с использованием модулей (Ninject Module).
public class LibraryModule : NinjectModule
{
    public override void Load()
    {
        Bind<IBook>().To<LibraryBook>();
        Bind<ILibraryBookService>().To<LibraryBookService>();
        Bind<IVisitorRepository>().To<VisitorRepository>();
        Bind<MainViewModel>().ToSelf();
        Bind<MainWindow>().ToSelf();
    }
}
Использование данного модуля:
var kernel = new StandardKernel(new LibraryModule());
var model = kernel.Get<MainViewModel>();
var view = kernel.Get<MainWindow>();
view.DataContext = model;
view.Show();
К сожалению, базового функционала, который идет в IoC контейнере Ninject, может быть недостаточно для Вашего приложения, поэтому нужно будет загружать необходимые дополнения. Ninject спроектирован по принципу: базовый функционал плюс дополнительные возможности, которые можно установить отдельно. Например, если Вы сторонник NHibernate и конфигурирования через файл конфигурации и не знакомы со Spring.Net (он отлично подходит для настроек NHibernate), посмотрите в сторону данного IoC контейнера. Пример с NHibernate был просто приведен для сравнения, чтобы показать, как дополнительные библиотеки Ninject позволят ускорить процесс разработки.

Constrain Resolution
Очень часто при разработке программного обеспечения приходится сталкиваться с тем, что от одного интерфейса наследуется несколько классов. А класс, который использует данный интерфейс, должен знать, с каким из вышеперечисленных вариантов его нужно использовать. Одним из самых простых вариантов решения данной проблемы является именованная привязка (Named binding). Продемонстрируем именованную привязку в электронной библиотеке, которая использовалась для примеров, добавив класс, который будет отвечать за работу с домашней коллекцией книг. Назовем этот класс HomeLibraryBookService, который наследуем от интерфейса ILibraryBookService. Ниже будет приведена тестовая реализация для демонстрации использования данного класса. В списке источников в конце статьи Вы можете скачать исходные коды к данной статье и ознакомится более подробно с использованием Ninject.
public class HomeLibraryBookService : ILibraryBookService
{
    #region Variable
    private ObservableCollection<IBook> _books;
    #endregion

    #region Constructor
    public HomeLibraryBookService()
    {
        _books = new ObservableCollection<IBook>();
        _books.Add(new LibraryBook { Author = "Test1", Title = "Test Unity1", Count = 3, SN = "ISBN: 9781617291340", Year = new DateTime(2013, 9, 10) });
        _books.Add(new LibraryBook { Author = "Test2", Title = "Test Unity2", Count = 2, SN = "ISBN-10: 0201485672", Year = new DateTime(1999, 7, 8) });
    }
    #endregion

    #region Public Methods
    public void GetData(Action<ObservableCollection<IBook>, Exception> callback)
    {
        callback(_books, null);
    }

    public IBook FindBook(IBook findBook)
    {
        if (findBook == null)
            return null;

        return _books.FirstOrDefault(book => book.Author == findBook.Author
                                    && book.Title == findBook.Title
                                    && book.SN == findBook.SN
                                    && book.Year == findBook.Year);
    }

    public void CreateNewBook()
    {
        _books.Add(new LibraryBook { Author = "UnityTest1", Title = "Test1", Count = 5, SN = "ISBN-10: 0735667454", Year = DateTime.Now });
    }

    public void RemoveBook(IBook book)
    {
        if (book == null)
            return;

        _books.Remove(book);
    }
    #endregion
}
Интерфейс ILibraryBookService используется в модели представления MainViewModel, которая отвечает за связывание моделей с представлением для проекта “Электронная библиотека”. Реализация представлена ниже.
public class MainViewModel : NotifyModelBase
    {
        private readonly ILibraryBookService _libraryDataService;

        public MainViewModel(ILibraryBookService dataService)
        {
            _libraryDataService = dataService;
            _libraryDataService.GetData(
                (items, error) =>
                {
                    Books = items;
                });
        }
}
С данного класса для был приведен только необходимый код для модифицированного примера использования. Посмотрим, как изменился класс LibraryModule со внесением именованной привязки.
public class LibraryModule : NinjectModule
{
    public override void Load()
    {
        Bind<IBook>().To<LibraryBook>();
        Bind<ILibraryBookService>().To<HomeLibraryBookService>().Named("HomeLibrary");
        Bind<ILibraryBookService>().To<LibraryBookService>().Named("Library");
        Bind<IVisitorRepository>().To<VisitorRepository>();
        Bind<MainViewModel>().ToSelf();
        Bind<MainWindow>().ToSelf();
    }
}
К сожалению, сразу после внесения проект не запустится, так как нужно указать, какой класс будет использоваться при constructor injection в классе MainViewModel. Поэтому в данном классе изменим немного вызов конструктора.
public MainViewModel([Named("Library")]ILibraryBookService dataService)
В данном примере был рассмотрен один из самых простых способов решения ограничения по привязке.
Другим способом является ограничения связывания с помощью использования ConstraintAttribute. Поскольку в примере с электронной библиотекой сложно представить, как использовать данный подход, возьмем пример, предоставленный разработчиками  Ninject.
// will work just as well without this line, but it's more correct and important for IntelliSense etc.
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter,
    AllowMultiple = true, Inherited = true)]
public class Swimmer : ConstraintAttribute
{
    public bool Matches(IBindingMetadata metadata)
    {
        return metadata.Has("CanSwim") && metadata.Get<bool>("CanSwim");
    }
}
class WarriorsModule : Ninject.Modules.NinjectModule
{
    public override void Load()
    {
        Bind<IWarrior>().To<Ninja>();
        Bind<IWarrior>().To<Samurai>().WithMetadata("CanSwim", false);
        Bind<IWarrior>().To<SpecialNinja>().WithMetadata("CanSwim", true);
    }
}
class AmphibiousAttack
{
    public AmphibiousAttack([Swimmer]IWarrior warrior)
    {
        Assert.IsType<SpecialNinja>(warrior);
    }
}

Итоги
Ninject - это мощный и одновременно легкий в использовании IoC контейнер. По простоте использования, а также понятности api этот контейнер чем-то напоминает Autofac. Данный контейнер имеет очень мощную поддержку в онлайн-обществе. Пожалуй, единственным нюансом, который может смущать в данном IoC контейнере, - это скорость его работы. Почему-то этот контейнер для управления зависимостями уступает по скорости всем описанным мной ранее IoC контейнерам. Но если у Вас небольшое приложение, и Вам не нужно создавать много объектов через данный контейнер, то в плане простоты этот контейнер управления зависимостями - именно то, что нужно.

Список литературы:

No comments:

Post a Comment