Любовь или брак по расчету с Dependency Injection?

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

Итак, суть проблемы. На проекте мы используем OData WebApi и все контроллеры наследуются от базового, используют метод GetService из базового класса который вытягивает зависимости через статический класс ApiControllerScopeContextMediator.

public abstract class ODataControllerBase : ODataController
{		
	protected T GetService<T>()
        {
              return ApiControllerScopeContextMediator.GetService<T>(this);
        }		
}	
internal static class ApiControllerScopeContextMediator
 {		
	internal static T GetService<T>(ApiController controller)
        {
              return (T) controller.Configuration.DependencyResolver.GetService(typeof (T));
        }		
}
	

А в Global.asax конфигурируем подтягивание зависимостей для OData через StructureMap:

    GlobalConfiguration.Configuration.DependencyResolver = new StructureMapDependencyResolver(container);

Во всех action у контроллеров повсеместно используется метод GetService, как, например, здесь:

public class DisconnectedAppsController : ODataControllerBase
{
	public IHttpActionResult Get()
        {		
		var query = GetService<IQuery<IQueryable<DisconnectedAppDomain>, DisconnectedAppFilter>>();		
	}
}

Но почему? Ведь можно было бы просто использовать constructor injection:

public DisconnectedAppsController(IQuery<IQueryable<DisconnectedAppDomain>, DisconnectedAppFilter> query){
    _query = query;
}

Так что же все-таки: «Таити, Таити» (Constructor Injection) или «нас и здесь неплохо кормят» (GetService)?

Какие проблемы с этим кодом я вижу:

  • Контроллеры нельзя покрыть unit тестами (без инициализации IoC контейнера)
  • Согласно принципу KISS нам не нужна дополнительная сложность, а согласно YAGNI дополнительная завязка на System.Web.Http.Configuration.DependencyResolver, которая вылезет нам боком если мы захотим перейти с MVC5 на MVC6, у которого в корне изменится работы с Dependency Injection, а ApiController в бета версии уже лишился Configuration
  • Взглянув на класс мы не видим явно все зависимости класса
  • Использование GetService у DependencyResolver — это по сути использование реализации IoC при помощи Service Locator, что является анти-паттерном
  • а Service Locator нарушает принципы SOLID
  • Мы нарушаем фундаментальный принцип ООП инкапсуляцию

Какие аргументы «против» мне довелось услышать:

  • В идеале контроллеры должны быть слишком тупы, чтобы их покрывать тестами, нам это никогда не понадобится, если и покрывать их, то интеграционными тестами
  • Отказываться от текущей реализации основываясь на ссылках в гугле слишком идеалистично, это не принесет пользы
  • Удобно для разработчика, меньше кода писать
  • Изменение кода, который используется подобным образом во многих местах, внесет энтропию в проект
  • Метод YAGNI в другой плоскости — а зачем нам менять что-то, если нет очевидной ежеминутной выгоды
  • Constructor injection в контроллерах банально неудобен

Пару лет назад прочитал книгу Марка Симана «Dependency Injection». Сижу и думаю, так что у меня с DI: любовь или брак по расчету?

Использованные материалы:

Mark Seeman «Dependency Injection»
Mark Seeman's blog
Microsoft MVC6 github open source project
SOLID wiki page
YAGNI wiki page
KISS wiki page
Share post

Similar posts

Comments 78

    +2
    На мой взгляд единственный серьёзный аргумент «против» — использование одновременно двух подходов. Остальные — надуманны.
    Чем неудобен Constructor Injection?
    Писать больше кода? А если сервис нужен в нескольких местах? Писать
    GetService<IQuery<IQueryable<DisconnectedAppDomain>, DisconnectedAppFilter>>();
    

    несколько раз?
      +2
      И ещё. При использовании Constructor Injection в случае невозможности разрешить зависимость вы получите ошибку сразу при создании объекта зависимого класса, а не неизвестно когда в процессе работы.
        0
        [сарказм] Именно для таких ситуаций и существует паттерн проектирования Helper/Utils!
          0
          В идеале контроллеры должны быть слишком тупы, чтобы их покрывать тестами, нам это никогда не понадобится, если и покрывать их, то интеграционными тестами


          Подход с «тонкими» контроллерам используют в Rails среде, когда пишут RESTful API Application. С ним действительно упрощается тестирование.

          Тестами покрываются только модели и функционирование UI. При этом, т.к. основное приложение чисто API провайдер, то UI реализуется и тестируется отдельно (например, с AngularJS либо EmberJS).

          Лично я вижу «зерно» в этом доводе.
          +1
          Constructor injection в контроллерах банально неудобен

          Почему?
            +1
            На мой взгляд Constructor injection вполне себе удобен, но в качестве контраргумента довелось слышать, что конструктор разрастается при и при 5 и больше зависимостях выглядит тяжеловесно. К тому же переданные в конструктор параметры нужно инкапсулировать в классе, что добавляет телодвижений.
            Но на мой взгляд простота, наглядность и очевидность зависимостей класса в купе с другими преимуществами являются достаточным мотивом для рефакторинга такого кода.
              +5
              конструктор разрастается при и при 5 и больше зависимостях выглядит тяжеловесно

              Обычно в таких случаях принято говорить «а не слишком ли много у вас ответственностей, если вам нужно 5+ зависимостей?»

              К тому же переданные в конструктор параметры нужно инкапсулировать в классе, что добавляет телодвижений.

              Ничем не тяжелее явного запроса зависимости при каждой необходимости.
                0
                Сделайте агрегированный сервис. В том же Autofac — Aggregate Services.

                Нажать Alt-Enter + Enter для автоматического создания филда и присваивания ему значения тяжело? Как же вы код то пишете?)

                Где у вас очевидность зависимостей то?
                  0
                  Где у вас очевидность зависимостей то?

                  Когда зависимости передают параметрами в конструктор они становятся очевидными, не нужно заведомо знать, что где-то используется GetService зависящий от чего-то.
                  Фишка решарпера Ctrl+Alt+D (при схеме горячих клавиш JetBrains) на ура решает проблему вынесения параметра конструктора в поле класса.
                  Поэтому я двумя руками «за» Constructor Injection. Для меня это весомый фактор, мотивирующий на рефакторинг существующего большого количества контроллеров. Ищу способы замотивировать, донести плюсы такого подхода коллегам.
                    0
                    Я и говорю, что GetService — порочная практика, как бы ни жаловались на количество параметров конструктора. Всегда есть способы уменьшить кол-во параметров, да и в принципе, этот код, с высокой вероятностью, будет первым кандидатом на декомпозицию.

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

                    Вы как -то путаетесь в показаниях. То «Constructor injection в контроллерах банально неудобен», то «На мой взгляд Constructor injection вполне себе удобен» ;)
                      0
                      Моё-то как раз мнение, как в самой статье, так и в комментариях — одинаково. Использовать так GetService это порочная практика.
                      Какие проблемы с этим кодом я вижу:

                      речь как раз идет о коде с GetService
                      На мой взгляд Constructor injection вполне себе удобен, но в качестве контраргумента довелось слышать, что конструктор разрастается при и при 5 и больше зависимостях выглядит тяжеловесно. К тому же переданные в конструктор параметры нужно инкапсулировать в классе, что добавляет телодвижений.

                      Тем не менее этот контраргумент вовсе не перевешивает «плюсы» Constructor injection.
                        0
                        А о чем тогда статья?) Показать как пользоваться тем, что уже давно не рекомендуется использовать? :)
                0
                Потому, что удобнее run method injection ;)
                  0
                  Аргументируйте.
                    +2
                    Нет необходимости определять конструктор и свойства контроллера.
                    Каждый конкретный обработчик конструктора может иметь отличные от других обработчиков зависимости.
                      +2
                      Каждый конкретный обработчик конструктора может иметь отличные от других обработчиков зависимости.

                      Наверное, вы имели в виду, обработчик контроллера, проще говоря — action?

                      Во-первых, часто ли вам надо иметь у них разные зависимости? А если у них у всех есть одна и та же зависимость, вы получите неслабое дублирование кода (причем если у вас внутри контроллера есть общие методы, то зависимость придется прокидывать и в них).

                      Во-вторых, обычно параметрами action являются данные, пришедшие в запросе — как вы планируете их разделять?

                      Ну и в-четвертых, неудобно иметь разные DI-методологии в разных частях системы.
                        0
                        Ну, подходы же можно и объединить: некоторые зависимости — в конструктор, а некоторые — в action.

                        Разделять данные и сервисы можно атрибутами. Или просто по наличию сервиса в контейнере. Лишь бы не по наличию данных в запросе :)

                        По поводу же разных методологий в разных частях системы — что именно в этом неудобного? Если, конечно, таких методологий 2-3, а не 10-20.
                          –2
                          Ну, подходы же можно и объединить: некоторые зависимости — в конструктор, а некоторые — в action.

                          См. ниже про разные методологии.

                          Разделять данные и сервисы можно атрибутами.

                          Лишняя маркировка.

                          Или просто по наличию сервиса в контейнере.

                          Можно случайно получить непредсказуемое поведение.

                          По поводу же разных методологий в разных частях системы — что именно в этом неудобного? Если, конечно, таких методологий 2-3, а не 10-20.

                          То что теперь, когда я хочу это протестировать, мне надо посмотреть в два-три места вместо одного. Вообще, любая неуниформность увеличивает стоимость поддержки.
                          0
                          Наверное, вы имели в виду, обработчик контроллера, проще говоря — action?

                          Совершенно верно, спросони опечатался.
                          Во-первых, часто ли вам надо иметь у них разные зависимости? А если у них у всех есть одна и та же зависимость, вы получите неслабое дублирование кода

                          С одной стороны — да, мне частенько нужны различные зависимости в разных методах контроллера. Как минимум очень часто в методах используются разные Mapper'ы. Если же большинство методов использует одну зависимость, то не исключается применение constructor injection именно для этой зависимости.
                          Во-вторых, обычно параметрами action являются данные, пришедшие в запросе — как вы планируете их разделять?

                          У меня это обычно делается так:
                          public function editAction($require, $urlParams){
                            ...
                          }
                          

                          то есть части URL, выделенные роутером хранятся в зависимости $urlParams, а параметры запроса, переданные клиентом, в $require. И то, и другое хранится в локаторе, что позволяет разрешить их с помощью DI.
                          Ну и в-четвертых, неудобно иметь разные DI-методологии в разных частях системы

                          Почему же?
                            0
                            А нету чего-то типа this.Request или this.Request.GetRouteData()? ;) Мапинг вообще делается обычно до вызова метода контроллера(ну во вселенной Asp.Net MVC/WebApi). Просто инъекция в аргументы метода как-то вообще странно выглядит.
                              0
                              А нету чего-то типа this.Request или this.Request.GetRouteData()?

                              this вещица хорошая, но она завязывает зависимости на уровне класса, а часто зависимости относятся только к методу. Приведу пример (на PHP, если вы не возражаете):

                              <?php
                              class UserController{
                                private $session;
                              
                                public function __construct($session){
                                  $this->session = $session;
                                }
                              
                                public function register($require, $userTable){
                                  $login = $require->param('login');
                                  $pass = $require->param('pass');
                              
                                  ...
                                  $id = $userTable->insert(['login' => $login, 'password' => $pass]);
                                  $this->session->save(['id' => $id, 'login' => $login]);
                                }
                              
                                public function logout(){
                                  $this->session->delete();
                                }
                              }
                              


                              Из примера видно, что общие зависимости, такие как Сессия, разрешаются через конструктор, но частные зависимости, разрешаются для каждого метода отдельно. Здесь зависимости require и userTable не нужны в методе logout, так зачем их разрешать на уровне конструктора? На деле это только осложняет тестирование, так как придется Mock'ать их при тестировании всех методов контроллера, даже если эти зависимости не нужны.

                              У меня реализуется это тоже довольно просто:

                              <?php
                              $manager = new Di\Manager;
                              $manager->set('require', $require);
                              $manager->set('session', $session);
                              $manager->set('userTable', function(){
                                ...
                                return $userTable;
                              });
                              
                              // Инстанциация контроллера через разрешение зависимостей конструктора
                              $controller = $manager->constructInjection($controllerName);
                              // Вызов экшена контроллера с разрешением его зависимостей
                              $result = $manager->methodInjection($controller, $action);
                              $response->body($result);
                              $response->send();
                              

                                0
                                Имхо профиту особо я не вижу, а вот проблем может добавить, особенно если баловаться с интроспекцией параметров, проксированием и тд.

                                С инъекцией в конструкторе все то же выглядит весьма прилично:
                                        public AccountController(
                                            UserManager<ApplicationUser> userManager,
                                            SignInManager<ApplicationUser> signInManager)
                                        {
                                            _userManager = userManager;
                                            _signInManager = signInManager;
                                        }
                                
                                        public async Task<IActionResult> Register(RegisterViewModel model)
                                        {
                                            if (ModelState.IsValid)
                                            {
                                                var user = new ApplicationUser {UserName = model.Email, Email = model.Email};
                                                var result = await _userManager.CreateAsync(user, model.Password);
                                                if (result.Succeeded)
                                                {
                                                    await _signInManager.SignInAsync(user, isPersistent: false);
                                                    return RedirectToAction(nameof(HomeController.Index), "Home");
                                                }
                                            }
                                
                                            return View(model);
                                        }
                                


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

                                  Почему же тогда я не встечаю классы вида:
                                  class MyClass{
                                    ...
                                    public function __construct($x, $y, $z, $a, $b, $c, ...){
                                      ...
                                    }
                                  
                                    public function methodA(){}
                                  
                                    public function methodB(){}
                                  
                                    public function methodC(){}
                                  }
                                  
                                    0
                                    Просто ошибки композиции в 99% случаев.
                              –1
                              да, мне частенько нужны различные зависимости в разных методах контроллера.

                              Вы уверены, что это не признак нарушения SRP?

                              Как минимум очень часто в методах используются разные Mapper'ы.

                              Ыыы, а у нас просто один маппер позволяет мапить все объекты.

                              части URL выделенные роутером хранятся в зависимости $urlParams, а параметры запроса, переданные клиентом, в $require.

                              Это все прекрасно, но где зависимости?

                              (Впрочем, вру, не прекрасно. Прекрасно — это вот так:

                              public async Task Put(string id, Location data)
                              {...}
                              

                              )

                              И то, и другое хранится в локаторе, что позволяет разрешить их с помощью DI.

                              Но зачем? И, главное, по каким признакам?

                              Почему же?

                              Выше уже ответил. Униформность нарушается.
                                –1
                                Ыыы, а у нас просто один маппер позволяет мапить все объекты

                                Один контроллер на все запросы не используете? )

                                Это все прекрасно, но где зависимости?

                                Внутри метода контроллера он выделяет нужные ему параметры и валидирует их.

                                Прекрасно — это вот так

                                Реализовывал подобное, да это удобно, но я вынес это на уровень аннотаций, так как в PHP все равно строго типизировать параметры не выходит.

                                Но зачем? И, главное, по каким признакам?

                                Сервисы должны хранится в локаторе.

                                Униформность нарушается

                                Тобишь если у нас есть одно рабочее решение, его нужно превращать в золотой молоток?
                                  +2
                                  Один контроллер на все запросы не используете? )

                                  Нет. Маппер — это сервис, у него нет бизнес-смысла, в отличие от контроллера.

                                  Внутри метода контроллера он выделяет нужные ему параметры и валидирует их.

                                  Это лишний повторяющийся код.

                                  Сервисы должны хранится в локаторе.

                                  Вот только пришедшие снаружи данные — это не сервисы. Что им делать в локаторе?

                                  Тобишь если у нас есть одно рабочее решение, его нужно превращать в золотой молоток?

                                  Если у нас есть рабочее решение, то не надо рядом внедрять почти такое же, но другое. Это будет путать программиста.
                                    –1
                                    Это лишний повторяющийся код

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

                                    Вот только пришедшие снаружи данные — это не сервисы. Что им делать в локаторе?

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

                                    Если у нас есть рабочее решение, то не надо рядом внедрять почти такое же, но другое. Это будет путать программиста

                                    Это самое «почти» имеет очень большое значение ) Превращать все частные зависимости методов в зависимости объекта, это золотой молоток.
                                      +2
                                      Который можно легко унифицировать и превратить в не повторящийся, что и было сделано с помощью аннотаций.

                                      Значит, это происходит не внутри метода контроллера.

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

                                      Зачем? Это, по факту, глобальное состояние.

                                      Превращать все частные зависимости методов в зависимости объекта, это золотой молоток.

                                      Понимаете ли, в чем дело: «зависимость» объекта — это его внутреннее дело. Сегодня они одни, а завтра — другие; сегодня они используются в одном методе, завтра — во всех. В то же время, параметры его методов — это его внешний контракт, чем меньше он меняется, тем лучше.
                                        0
                                        Значит, это происходит не внутри метода контроллера

                                        Все, чего бы вы не делали, можно выполнить как внутри некого метода, так и вне его. Не вижу аргументации в этом.

                                        Зачем? Это, по факту, глобальное состояние

                                        Чтобы можно было разрешить их как зависимость. Очевидно же.

                                        Сегодня они одни, а завтра — другие; сегодня они используются в одном методе, завтра — во всех. В то же время, параметры его методов — это его внешний контракт, чем меньше он меняется, тем лучше

                                        А семантика контроллера, это не контракт? )
                                          0
                                          Чтобы можно было разрешить их как зависимость. Очевидно же.

                                          А зачем вам разрешать состояние как зависимость?

                                          А семантика контроллера, это не контракт?

                                          Нет.
                                            0
                                            А зачем вам разрешать состояние как зависимость?

                                            Потому что это не состояние, а пакет данных пользователя, от которого зависят некоторые методы моего контроллера.

                                            Нет

                                            То есть в изменении семантики контроллера вы проблем не видите, а в изменении семантики других методов видите? )
                                              0
                                              Потому что это не состояние, а пакет данных пользователя, от которого зависят некоторые методы моего контроллера.

                                              Методы контроллера не могут зависеть от пакета данных пользователя, пакет данных — это входные данные, аргументы.

                                              а в изменении семантики других методов видите?

                                              Так в том-то и дело, что зависимости никак не определяют семантику метода. Поэтому меня и раздражает, что сугубо внутреннее устройство (зависимости) проползают во внешний контракт.
                                                0
                                                Методы контроллера не могут зависеть от пакета данных пользователя, пакет данных — это входные данные, аргументы

                                                Мысленно назовите всю совокупность этих данных пакетом и попробуйте сделать так, чтобы метод от них «не зависел».
                                                Так в том-то и дело, что зависимости никак не определяют семантику метода

                                                Еще как определяют.
                                                  0
                                                  Мысленно назовите всю совокупность этих данных пакетом и попробуйте сделать так, чтобы метод от них «не зависел».

                                                  У нас с вами какая-то терминологическая путаница. Функция (в математическом смысле) не «зависит» от своих аргументов. «Зависимость» метода (уже в программном смысле) — это какой-то объект, нужный ему для корректного выполнения задачи. Входные аргументы зависимостями не являются.

                                                  Еще как определяют.

                                                  Неа. Семантика метода, скажем, PutArticle — в том, чтобы сохранить статью (в некое хранилище). То, что при этом ему нужен сервис безопасности и (особенно) сервис логирования — это не его семантика.
                                                    0
                                                    «Зависимость» метода (уже в программном смысле) — это какой-то объект, нужный ему для корректного выполнения задачи

                                                    Почему именно объект? Если я обмениваюсь данными с сервером в виде JSON, то это уже зависимости?

                                                    То, что при этом ему нужен сервис безопасности и (особенно) сервис логирования — это не его семантика

                                                    Откуда же метод получит сервис логирования?
                                                      0
                                                      Почему именно объект?

                                                      Потому что обычно внутрение сущности программы в ООП — это объекты.

                                                      Если я обмениваюсь данными с сервером в виде JSON, то это уже зависимости?

                                                      Нет, это входные (или выходные) данные.

                                                      Откуда же метод получит сервис логирования?

                                                      А это не имеет никакого значения для его семантики. Более того, его семантика не изменится от того, использует он метод логирования или нет.
                                                        0
                                                        Потому что обычно внутрение сущности программы в ООП — это объекты
                                                        Нет, это входные (или выходные) данные

                                                        Которые «объекты», которые нужны для корректного выполнения задачи. Все сходится.
                                                        А это не имеет никакого значения для его семантики. Более того, его семантика не изменится от того, использует он метод логирования или нет

                                                        То есть не важно как метод получит свои зависимости, главное чтоб семантику ему не поменять? )
                                                          0
                                                          Которые «объекты», которые нужны для корректного выполнения задачи. Все сходится.

                                                          Не, не сходится. Входные данные — это не зависимости.

                                                          То есть не важно как метод получит свои зависимости, главное чтоб семантику ему не поменять?

                                                          Да, именно так.
                    +1
                    Суть проблемы в том, что вам нужно подружить два фреймворка, которые диктуют дизайн — DI Container и OData WebApi. Так как оба фреймворка хотят «контроллировать» конструктор, нужен какой-то "костыльадаптер" для того чтобы их подружить.

                    Вы описали типичную проблему интеграции и к самим принципам DI она не имеет отношения.
                      0
                      А в каком месте OData WebAPI «хочет контролировать конструктор»? (собственно, к DI Container тоже относится)
                        +1
                        DI Container — «создает граф объектов», «зависимости декларируются в конструкторе (обычно)».
                        OData WebAPI «содает контроллеры для обработки запросов», «наследуется базовый контроллер, который может дать ограничение на конструктор».

                        Соотвествено реализация IDependencyResolver и является адаптером, который позволяет интегрировать оба фреймворка.
                          –1
                          зависимости декларируются в конструкторе (обычно)

                          Могут, но не обязаны.

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

                          Наследуемый базовый контроллер не налагает никаких ограничений на конструктор.

                          Соотвествено реализация IDependencyResolver и является адаптером, который позволяет интегрировать оба фреймворка.

                          Неа. IDependencyResolver (за компанию с еще некоторыми классами) — это точка расширения, позволяющая использовать в WebAPI тот DI-контейнер, который нужен программисту. Совершенно штатная.

                            –2
                            Неа. IDependencyResolver (за компанию с еще некоторыми классами) — это точка расширения, позволяющая использовать в WebAPI тот DI-контейнер, который нужен программисту. Совершенно штатная.

                            Соглашусь с этим утверждением. Но сразу принял бы во внимание, что в MVC6 этой точки расширения уже не будет, поскольку DI-контейнер будет интегрирован в сам фреймворк
                              +2
                              Будет другая, app.ApplicationServices. Всей-то разницы.
                                0
                                internal static T GetService<T>(ApiController controller)
                                        {
                                              return (T) controller.Configuration.DependencyResolver.GetService(typeof (T));
                                        }
                                

                                Но от завязки на этот код в статическом классе придется избавляться.
                                  0
                                  Если вам очень захочется, то через ControllerContext доберетесь. Но учитывая, что ApiController тоже нет, вам в любом случае этот код переписывать.
                      –3
                      Неа. IDependencyResolver (за компанию с еще некоторыми классами) — это точка расширения, позволяющая использовать в WebAPI тот DI-контейнер, который нужен программисту. Совершенно штатная.


                      Что собственно я и написал. И нужен он потому что оба фреймворка завязаны на использование конструкторов. Вы или читать не умеете или просто некомпетентны.
                        0
                        Как раз наоборот, ни один из фреймворков (более того, DI-контейнер — это не фреймворк) не завязан на использование конструкторов (насколько вообще создание объектов в .net может не быть завязано на использование конструктора).

                        В WebAPI контроллеры создаются через IHttpControllerActivator, который делегирует создание в IDependencyResolver, а за его отсутствием вызывает Activator.CreateInstance. OData не накладывает никаких ограничений на конструктор: у стандартного конструктора нет параметров.

                        Весь смысл этой точки расширения в том, чтобы дать пользователю возможность использовать свой DI. Конструкторы, не конструкторы — никого уже не волнует. Хоть синглтоны.
                        0
                        Чтобы минимизировать синтаксический оверхед от contructor injection, я пользуюсь таким шаблоном:
                            public class HomeController : Controller
                            {
                                [Dependency]
                                public IService1 Service1 { get; set; }
                        
                                [Dependency]
                                public IService2 Service2 { get; set; }
                                
                                public HomeController(IUnityContainer uc)
                                {
                                    uc.BuildUp(this);
                                }
                        
                                public ActionResult Index()
                                {
                                    var data = Service1.Load();
                                    // ...
                                }
                            }
                          +3
                          Сдаётся мне, если конструктор полностью убрать, работать будет точно так же.
                            0
                            Для ASP.NET MVC из коробки — нет. Надо что-то донастроить, пока не нашёл, что.
                              0
                              Да, работает!
                              DependencyResolver.SetResolver(new UnityDependencyResolver(uc));
                              +3
                              … и теперь ваш контроллер, как и его тесты, напрямую зависит от DI-контейнера.
                                0
                                Да. Но я ещё не встречал проекта, где на полпути меняли бы БД, ORM или контейнер.
                                Обычно в таких ситуациях переписывают с нуля «сразу правильно».
                                  0
                                  Тут не в смене дело. Ваш контроллер теперь узнал о существовании Unity. В том же Autofac достаточно зарегестрировать с AutowireProperties и не надо в класс контроллера добавлять такие штуки. Плюс еще и зависимость на контейнер таскать придется.
                                    0
                                    Нужны ли юнит-тесты на контроллеры? Я стараюсь всё сложное переносить в сервисы, и их тестировать.
                                    Основная работа контроллера — наполнение ViewBag, зачастую код во View сложнее, чем в контроллере. Если и тестировать, то связку Controller+View, а выходной HTML из View непонятно, как тестировать (у кого-то есть опыт?).
                                      0
                                      Да, нужны. Content negotiation, генерация роутов, локализация(в том числе и вьюх). Например простейший тест на локализацию — поиск временных строковых токенов в html. Да даже проверить тот же ViewBag. А еще проверить обработку исключений например и их мапинг в статус коды. В общем надо:)
                                        0
                                        Razaz — в качестве альтернативы юнит тестам в этой ситуации, чтобы отловить указанные моменты в работе контроллера Web API или MVC приложения можно делать в тестах запросы через HttpClient или подобными способами — http://www.diogonunes.com/blog/webclient-vs-httpclient-vs-httpwebrequest/

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

                                        Думаю, с помощью таких вот интеграционных тестов на контроллеры (покрыващих вопросы контента, маршрутизации, и тп) — вполне можно смириться с непокрытием их обычными юнит тестами. Особенно если (как справедливо заметил qw1 ) логику держать все-таки в сервисах и юнит-тестировать их.

                                        Вполне все удобно получается. И зависимости контроллера не помешают. Для интерграционных тестов достаточно иметь поднятое окружение для тестирования. URL прописать в конфиг его для тест проекта. Локально чтобы на локальной машине гонялись, На билд сервере — на тестовом окружении.
                                          0
                                          Проблема как раз в том, что являются интеграционными :)
                                          Мне как-то не нравится оставлять не покрытыми контроллеры, которые участвуют в реализации каких-либо протоколов(OAuth, OIDC, SCIM) например :) Если можно покрыть тестами, то почему нет? Плюс не всегда есть возможность на каждый билд разворачивать новый тестовый инстанс приложения.
                                          Плюс ко всему прочему страдает атомарность тестов. При тестировании контроллеров я могу сразу понять где проблема — в контроллере, в фильтре или в middlware. При интеграционном тесте можно долго ковыряться с тем, что и где пошло не так.
                                            0
                                            Если нет continuous integration и тестовое окружение не доступно на каждый билд — это, конечно, сложнее. В остальном — как раз вызовом API программно через вебклиента вы по сути лучше проверяете взаимодействие с вашим приложением извне — более честно. Ведь методы контроллера никто напрямую не вызывает как библиотечные. Соответственно, чтобы задействовать все праоверки доступа, маршрутизацию и тп — как раз и удобно подергать контроллер через URL.

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

                                            Так не думаете?
                                              0
                                              Окружение доступер, но тест охватывает слишком большой кусок функциональности за раз, при этом не предоставляя возможности протестировать отдельные части. Можно все протестировать без вызова контроллера извне. Плюс с такими тестами гораздо проще отладиться, так как не надо постоянно слать запросы — достаточно просто подебажить нужный кусок.
                                    0
                                    Но я ещё не встречал проекта, где на полпути меняли бы БД, ORM или контейнер.

                                    Я вот пару недель назад менял на проекте DI-контейнер.
                                      0
                                      А я в паре проектов менял DI контейнер.
                                        0
                                        Если не секрет, на какой, и что он дал такого, чего не было в предыдущем?
                                          +1
                                          Менял с Unity на Autofac. В Autofac лучше управление временем жизни зависимостей, а это оказалось важно.
                                        0
                                        Значит проекты были не поворотливыве.
                                        В парочке больших проектов, мы уже пару раз меняли ORM — максимум неделя, и 1-2 дня отлов багов. Это не страшно.

                                        DI — мы ещё не меняли, не видем в этом смысла Unity хоть и тежеловат по сравнению с конкурентами, но его гибкость пока недостежимая высота для других.
                                          0
                                          Мне прямо интересно, что такого вы смогли добиться от Unity, чего не может Autofac? (Кроме интерцепции)
                                            0
                                            Ну Interception то же практически из коробки с DynamicProxy ;)
                                      0
                                      Конструктор, принимающий контейнер DI — это антипаттерн, поскольку вносит в класс дополнительную зависимость, не упрощая при этом ничего.

                                      Сравните: new HomeController(uc) и uc.Resolve<HomeController>(). Первый способ создания объекта ничуть не проще второго.

                                        0
                                        Я не делаю new HomeController(uc). Если бы создание контроллера было в моём коде, я бы написал через Resolve, но поскольку ASP.NET MVC делает constructor injection, тут такой костыль.
                                          +1
                                          поскольку ASP.NET MVC делает constructor injection, тут такой костыль.

                                          asp.net MVC не делает constructor injection. asp.net MVC передает запрос в IDependencyResolver, а тот — вашему DI-контейнеру.
                                            +1
                                            Это всё SO-driven programming )) Если изучить документацию перед написанием кода, можно упростить код. Танцы с BuildUp в конструкторе не нужны.
                                      +1
                                      В идеале контроллеры должны быть слишком тупы, чтобы их покрывать тестами, нам это никогда не понадобится, если и покрывать их, то интеграционными тестами


                                      Я за этот вариант, потому что есть дополнительный плюс. Представьте что в контроллере у вас есть интересный код, который потребовался вам, ну например, в вебджобе, или в другом сайте. Что будете делать зависимость от этого веб проекта? Нет, придется утаскивать в некоторую библиотеку. Поэтому можно сразу делать все этой библиотеке, а в контроллерах оставлять только самый наитупейший код, который работает на очень высоком уровне абстрагирования, такой код и тестировать не захочется. Один минус что придется пробрасывать вызовы из контроллера в вашу либу, ну да и ладно, зато уходим от всех проблем борьбы с чужими фреймворками
                                        0
                                        Добавлю и свои 5 копеек.

                                        1. constructor injection — это читаемость кода. Сразу видно, что необходимо для работы конкретного класса.

                                        к примеру:
                                        public class MyController: Controller
                                        {
                                        ctor(IReader reader, IWriter writer);
                                        }

                                        и любому читающему понятно, что тут R/W операции, не вчитываясь в остальные строчки кода.

                                        Использование injection method — это штуковина, нужна для экзотических вещей, когда в время построения нужно преодолеть рекурсивную зависимость объектов один от другого. Ну и ещё для парочки специфических сценариев.

                                        Если у класса есть опциональные зависимости, то такие зависимости лучше подсовывать через property injection.
                                        Вообщем, правило должно быть такое: то, без чего работа класса не возможна — в конструктор, всё остальное property injection.
                                          +1
                                          Property Injection в принципе рекомендуется только для специфических случаев. Например, если каждый класс использует ILogger или используется наследование. Ну или если конструктор нельзя трогать(Сейчас это вообще редкость).
                                          Что это за опциональные зависимости такие? Типа есть работаю, а нет начинаю менять поведение? :)
                                            0
                                            Что это за опциональные зависимости такие? Типа есть работаю, а нет начинаю менять поведение?

                                            Да. Достигается с помощью создания нескольких конструкторов, либо, как было сказано выше, через property injection. Есть мнение, и я с ним согласен по большому счёту, что это антипаттерн.
                                              0
                                              Я понимаю, что можно баловаться всякими Constructor Selector и подобными фокусами, просто давно уже не возникала необходимость в таком хитром трюке. Полностью согласен с автором статьи.

                                        Only users with full accounts can post comments. Log in, please.