Страницы

Поиск по вопросам

среда, 5 февраля 2020 г.

C#,ASP.NET MVC IoC-контейнер Ninject [закрыт]

#c_sharp #aspnet #aspnet_mvc #ninject


        
             
                
                    
                        
                            Закрыт. Данный вопрос необходимо конкретизировать. Ответы
на него в данный момент не принимаются.
                            
                        
                    
                
                            
                                
                
                        
                            
                        
                    
                        
                            Хотите улучшить этот вопрос? Переформулируйте вопрос,
чтобы он был сосредоточен только на одной проблеме, отредактировав его.
                        
                        Закрыт 3 года назад.
                                                                                
           
                
        
Объясните на пальцах про IoC-контейнер Ninject — как используется и в целом зачем
все это надо? 

Читаю литературу, никак не могу понять. Лучше на практике один раз увидеть.
    


Ответы

Ответ 1



На пальцах вряд ли получится, тема сложная. Возникла она приблизительно тогда же, когда объектно-ориентированные языки стали применять для разработки многозвенных приложений. Типичное многозвенное приложение состоит из трёх уровней: уровня представления, уровня предметной области и уровня доступа к данным. В классической схеме зависимость между уровнями распространяется сверху вниз: уровень представления зависит от уровня предметной области, а тот, в свою очередь — от уровня доступа к данным. +----------------------------+ | Уровень представления | +----------------------------+ \/ +----------------------------+ | Уровень предметной области | +----------------------------+ \/ +----------------------------+ | Уровень доступа к данным | +----------------------------+ Рассмотрим простой пример с веб-приложением. Пусть, например, у нас есть список заказов, которые мы хотим показывать пользователю: public OrderController : Controller { [HttpGet] public ActionResult Index() { var userId = GetCurrentUserId(); var repository = new OrderRepository(); var orders = repository.GetAllByUserId(userId); return View(orders); } } Для рендеринга используем простую разметку: @model IEnumerable @{ ViewBag.Title = "Список заказов"; } @foreach (var order in Model) { }
Номер Дата Сумма
@order.Number @order.Date @order.Amount
Это уровень представления, где конечный пользователь видит список заказов на сайте. Чтобы получить этот список, мы обращаемся к хранилищу заказов (это класс уровня предметной области) и получаем заказы (тоже предметная область). Список этих заказов мы отправляем в движок рендеринга HTML, который и превратит их в веб-страницу. А вот как выглядит код уровня доступа к данным, если обращается к данным через Entity Framework: public OrderRepository { public IReadOnlyCollection GetAllByUserId(int userId) { using (var dbContext = new MyDbContext()) { return dbContext.Orders .AsNoTracking() .Where(x => x.UserId == userId) .AsEnumerable() .Select(x => new Order(x.Id, x.Number, x.Date, x.Amount)) .ToArray(); } } } Мы загружаем из БД сырые данные, и создаём из них классы предметной области, обладающие также и поведением. Классы уровня доступа к данным, это, например, MyDbContext и OrderData. Получается вроде бы всё хорошо и расширяемо, но. Что нужно, чтобы перейти в таком приложении от веб интерфейса к оконному? В идеале — написать один новый уровень представления, оконный. Два оставшихся уровня останутся прежними. Здорово. А что нужно, чтобы перейти в таком приложении с SQL на или MongoDb или файловое хранилище? Переписать все уровни, поскольку нижний переписывать всё равно придётся, а вслед за ним придётся переписывать всё, что от него зависит. Нездорово. Что можно сделать, чтобы упростить перенос таких приложений на другие хранилища? Инвертировать зависимость, то есть сделать так, чтобы уровень доступа данных зависел от уровня предметной области. +----------------------------+ | Уровень представления | +----------------------------+ \/ +----------------------------+ | Уровень предметной области | +----------------------------+ /\ +----------------------------+ | Уровень доступа к данным | +----------------------------+ Если мы сделаем так, то получится, что предметная область станет центральной. От неё будут зависеть уровни представления (веб, консоль, десктоп) и уровня доступа к данным (SQL, MongoDb, XML-файлы). Мы сможем расширять приложение, добавляя модули сверху и снизу, поскольку они будут зависеть только от уровня предметной области. Возникает вопрос: но ведь тогда появляется зависимость от центрального уровня предметной области? Что, если мы захотим переписать её? Ответ неожиданный: именно предметная область определяет всё приложение. Если это Word то в предметной области описаны такие штуки, как документы, параграфы, форматирование и всё остальное. В отличие от предыдущих случаев задача подменить предметную область просто бессмысленна, у вас получается другое приложение. Значит, инвертирование зависимости вещь полезная. Но как её осуществить практически? Практически мы должны описать абстракцию (интерфейс) доступа к данным. Вместо конкретного класса OrderRepository у нас появляется интерфейс: public interface IOrderRepository { IReadOnlyCollection GetByUserId(int userId); } Это интерфейс предметной области. Он используется в контролере OrderController нашего MVC приложения (то есть он доступен с уровня представления). public OrderController : Controller { private readonly IOrderRepository _orderRepository; public OrderController(IOrderReposiotory orderRepository) { _orderRepository = orderRepository; } [HttpGet] public ActionResult GetOrders() { var userId = GetCurrentUserId(); var orders = _orderRepository.GetAllByUserId(userId); return View(model); } } Код отличается от предыдущего тем, что уже не может создать объект класса OrderRepository непосредственно, более того, из контроллера этот класс совсем недоступен. Мы имеем доступ только к абстракции (интерфейсу) и ожидаем получить реализацию через конструктор контроллера. Кто-то снаружи должен создать объект OrderRepository и передать его экземпляр в наш конструктор. Пока отложим рассмотрение вопроса, кто этот «кто-то» и посмотрим, как репозиторий реализован на уровне доступа к данным. public OrderRepository : IOrderRepository { public IReadOnlyCollection GetAllByUserId(int userId) { using (var dbContext = new MyDbContext()) { return dbContext.Orders .AsNoTracking() .Where(x => x.UserId == userId) .AsEnumerable() .Select(x => new Order(x.Id, x.Number, x.Amount)) .ToArray(); } } } Здесь интерфейс одного уровня реализуется на другом уровне. Физически зависимость означает, что из проекта, где реализован OrderRepository должен стоять reference на проект, где описан IOrderRepository и в данном случае мы видим, что зависимость инвертирована: уровень доступа к данным зависит от уровня представления. Теперь, если мы захотим изменить представление, нам не надо менять OrderRepository, достаточно вместо веб-интерфейса реализовать другой, например, консольный интерфейс. Если мы захотим изменить реализацию с EF на NHibernate, нам достаточно будет переписать только репозитории, не трогая весь остальной проект, например, контроллеры. Остаётся вопрос: а кто же увязывает друг с другом интерфейсы и реализации? Тот самый IoC-контейнер, в частности, NInject. В проекте, где создается этот контейнер, сходятся все зависимости, поэтому он называется корнем композиции. Что нужно сделать? Нужно подменить стандартный IDependencyResolver из ASP.NET MVC своей реализацией на базе NInject и зарегистрировать в контейнере свои зависимости. О реализации написано, например, здесь. Регистрация выполняется в приватном методе AddBindings: public class NinjectDependecyResolver : IDependencyResolver { private readonly IKernel kernel; public NinjectDependecyResolver() { kernel = new StandardKernel(); AddBindings(); } public object GetService(Type serviceType) { return kernel.TryGet(serviceType); } public IEnumerable GetServices(Type serviceType) { return kernel.GetAll(serviceType); } void AddBindings() { kernel.Bind().To(); . . . } } Далее, в Global.asax меняем объект для разрешения зависимостей: protected void Application_Start() { DependencyResolver.SetResolver(new NinjectDepedencyResolver()); } После этого при попытке создать OrderController движок обратится к NinjectDependencyResolver, который обнаружит, что в конструкторе ожидается объект, реализующий интерфейс IOrderRepository. Движок NInject пройдёт по графу зарегистрированных объектов и выяснит, что этот интерфейс реализует класс OrderRepository который может быть создан непосредственно (то есть у него пустой конструктор). NInject создаст этот класс и передаст ссылку на него в конструктор OrderController. Этот паттерн называется внедрением через конструктор. Созданный контролер будет передан в движок ASP.NET MVC и обработает запрос Index. Помимо NInject существует большое количество IoC-контейнеров: Castle Windsor, Autofac, Unity. При желании, можно даже обрабатывать зависимости вручную, что иногда делают в небольших проектах. Подытожу основные моменты: Мы вводим интерфейсы между уровнем предметной области и уровнями более низких уровней (классически это уровень доступа к данным, в современных приложениях таких может быть больше, чем один). Объекты предметной области и вышележащих уровней для работы используют только интерфейсы, получая их через конструктор. Например, если нам в контролере нужен репозиторий, мы описываем параметр в конструкторе и сохраняем переданное значение в приватной переменной объекта. Мы реализуем все используемые интерфейсы на нижележащих уровнях. Мы сводим воедино вместе все интерфейсы и их реализации (в классической терминологии IoC — регистрируем (register) интерфейсы в контейнере, в терминологии NInject — назначаем (bind) реализацию). Все зарегистрированные объекты образуют граф объектов. Мы используем IoC-контейнер для создания классов верхнего уровня (в MVC приложении это контролеры). Чтобы это сделать, MVC предоставляет интерфейс IDependencyResolver и статический метод DependencyResolver.SetResolver. Контейнер разрешает все промежуточные зависимости, пользуясь графом объектов. Внизу графа находятся примитивные классы с пустыми конструкторами, которые создаются непосредственно. Всё это позволяет прозрачным образом реализовать инверсию зависимостей. Наш код становится проще. Модульные тесты разрабатывать проще. При необходимости, перейти с одной СУБД на другую СУБД или даже совсем не СУБД — такой переход тоже сделать проще.

Ответ 2



На пальцах (простой тупой пример с картинкой и почти без кода). Об ошибках и уточнениях пишите - поправлю. Чтобы лучше понять просто создайте тестовый проект, который будет из файлов разного формата считывать одни и те же данные (csv, json, xml, txt, любой другой по желанию). Или что-то аналогичное. Пусть я хочу написать сайт. Он будет отображать каталог какой-то продукции. Продукции всего 30 предметов и ожидается, что в ближайший год ее станет максимум 33 предмета. Я решил, что 30 предметов можно легко сохранить в JSON файле. Его можно легко редактировать руками и предметов в нем мало - ничего страшного. Создаю новое ASP.Net приложение. В нем будет контроллер CatalogController, отображение ListAllItems, класс ProductItem для одного продукта, класс для считывания файла ProductFileReader и класс для парсинга (он будет создавать список из ProductItem) ProductParser. Картинка: И тут начальник сказал что он хочет тащить список из 1С... . Придется переписать код, который использует ProductParser и ProductFileReader, потому что они уже не подходят. Тут, к счастью, все это используется только в CatalogController. Можно, конечно, все один раз переписать с мыслью, что ничего точно не поменяется (ага, никогда). Здесь приходит мысль, что в контроллере не требуется вызывать все это. Тут нужно получить только список продукции и откуда он возьмется нам все равно. То есть сюда можно ввести зависимость. От чего? Создадим интерфейс IProductProvider и будем в контроллере им пользоваться. Интерфейс (точнее класс который его реализует) даст нам список продукции и не важно как он получился и откуда он взялся - главное, что он есть в нужном месте. О создании нужного объекта для требуемого интерфейса позаботится, например, NInject. И эта зависимость будет только в одном месте (в настройках NInject). private static void RegisterServices(IKernel kernel) { kernel.Bind() .To(); } ProductProviderFromFile будет считывать файл и парсить. Можем потом поменять на ProductProviderFromDB, который получит список из базы. Тогда блок с контроллером будет выглядеть так: Таким образом переписывать контроллер не придется, если мы захотим перейти на хранение в базе. То же самое с модульным тестированием. Нам надо протестировать только контроллер отдельно от всего, а к нему гвоздями прибиты классы для считывания файлов и парсинга и от них легко не избавиться. Тут с помощью NInject или вообще без него можно просто подсунуть контроллеру любой другой класс который реализует интерфейс IProductProvider (он может просто вручную забитый список из трех тестовых продуктов возвращать - какая разница, хоть пустой).

Комментариев нет:

Отправить комментарий