Перевод статьи Field Dependency Injection Considered Harmful за авторством Vojtech Ruzicka

image

Внедрение зависимостей через поля является очень популярной практикой в DI-фреймворках, таких как Spring. Тем не менее, у этого метода есть несколько серьезных компромиссов и поэтому стоит чаще избегать его.

Типы внедрений


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

Конструктор


private DependencyA dependencyA;
private DependencyB dependencyB;
private DependencyC dependencyC;

@Autowired
public DI(DependencyA dependencyA, DependencyB dependencyB, DependencyC dependencyC) {
    this.dependencyA = dependencyA;
    this.dependencyB = dependencyB;
    this.dependencyC = dependencyC;
}

Сеттер


private DependencyA dependencyA;
private DependencyB dependencyB;
private DependencyC dependencyC;

@Autowired
public void setDependencyA(DependencyA dependencyA) {
    this.dependencyA = dependencyA;
}

@Autowired
public void setDependencyB(DependencyB dependencyB) {
    this.dependencyB = dependencyB;
}

@Autowired
public void setDependencyC(DependencyC dependencyC) {
    this.dependencyC = dependencyC;
}

Поле


@Autowired
private DependencyA dependencyA;
@Autowired
private DependencyB dependencyB;
@Autowired
private DependencyC dependencyC;

Что не так?


Как можно наблюдать, вариант внедрения через поле выглядит очень привлекательным. Он очень лаконичен, выразителен, отсутствует шаблонный код. По коду легко перемещаться и читать его. Ваш класс может просто сфокусироваться на основной функциональности и не загромождается шаблонным DI-кодом. Вы просто помещаете аннотацию @Autowired над полем — и все. Не надо писать специальных конструкторов или сеттеров только для того, чтобы DI-контейнер предоставил необходимые зависимости. Java довольно многословна сама по себе, так что стоит использовать любую возможность, чтобы сделать код короче, верно?

Нарушение принципа единственной ответственности


Добавлять новые зависимости просто. Возможно даже слишком просто. Нет никакой проблемы добавить шесть, десять или даже более зависимостей. При использовании конструкторов для внедрения, после определенного момента число аргументов конструктора становится слишком большим и тут же становится очевидно, что что-то не так. Наличие слишком большого количества зависимостей обычно означает, что у класса слишком много зон ответственности. Это может быть нарушением принципов единственной ответственности (single responsibility) и разделения ответственности (ориг.: separation of concerns) и является хорошим индикатором, что класс возможно стоит более внимательно изучить и подвергнуть рефакторингу. При использовании внедрения через поля такого явного тревожного индикатора нет, и таким образом происходит неограниченное разрастание внедренных зависимостей.

Сокрытие зависимостей


Использование DI-контейнера означает, что класс более не ответственен за управление его зависимостями. Ответственность за их получение выносится из класса во вне и теперь кто-то другой ответственен за их предоставление: это может быть DI-контейнер или ручное предоставление их через тесты. Когда класс более не отвечает за получение зависимостей, он должен явно взаимодействовать с ними, используя публичные интерфейсы — методы или конструкторы. Таким образом становится четко понятно, что требует класс, а также опциональные ли это зависомости (через сеттеры) или обязательные (конструктор)

Зависимость от DI-контейнера


Одна из ключевых идей DI-фреймворков заключается в том, что управляемый класс не должен зависеть от конкретного используемого контейнера. Другими словами, это должен быть простой POJO-класс, экземпляр которого может быть создан самостоятельно, если вы передадите ему все необходимые зависимости. Таким образом, вы можете создать его в юнит-тесте без запуска контейнера и протестировать его отдельно (с контейнером это будет скорее интеграционный тест). Если нет завязки на контейнер, вы можете использовать класс как управляемый или неуправляемый, или даже переключиться на другой DI-фреймворк.

Однако при внедрении прямо в поля вы не предоставляете прямого способа создания экземпляра класса со всеми необходимыми зависимостями. Это означает, что:

  • Существует способ (путем вызова конструктора по-умолчанию) создать объект с использованием new в состоянии, когда ему не хватает некоторых из его обязательных зависимостей, и использование приведет к NullPointerException
  • Такой класс не может быть использован вне DI-контейнеров (тесты, другие модули) и нет способа кроме рефлексии предоставить ему необходимые зависимости

Неизменность


В отличие от способа с использованием конструктора, внедрение через поля не может использоваться для присвоения зависимостей final-полям, что приводит к тому, что ваши объекты становятся изменяемыми

Внедрение через конструктор vs сеттер


Таким образом, инъекция через поля может не быть хорошим способом. Что остается? Сеттеры и конструкторы. Какой из них следует использовать?

Сеттеры


Сеттеры следует использовать для инъекций опциональных зависимостей. Класс должен быть способен функционировать, даже если они не были предоставлены. Зависимости могут быть изменены в любое время после создания объекта. Это может быть, а может и не быть преимуществом в зависимости от обстоятельств. Иногда предпочтительно иметь неизменяемый объект. Иногда же полезно менять составные части объекта во время выполнения — например управляемые бины MBean в JMX.
Официальная рекомендация из документации по Spring 3.x поощряет использование сеттеров над конструкторами:
Команда Spring главным образом выступает за инъекцию через сеттеры, потому что большое количество аргументов конструктора может стать громоздким, особенно если свойства являются необязательными. Сеттеры также делают объекты этого класса пригодными для реконфигурации или повторной инъекции позже. Управление через JMX MBeans является ярким примером

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

Конструкторы


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

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

Еще одним преимуществом является то, что при использовании Spring версий 4.3+ вы можете полностью отвязать ваш класс от конкретного DI-фреймворка. Причина в том, что Spring теперь поддерживает неявное внедрение через конструктор для сценариев использования с одним конструктором. Это означает, что вам больше не нужны DI-аннотации в вашем классе. Конечно, вы можете достигнуть того же результата с помощью явного конфигурирования DI в настройках Spring для данного класса; просто сейчас это сделать гораздо проще.

Что касается Spring 4.x, официальная рекомендация из документации изменилась и теперь инъекция через сеттер более не предпочтительна над конструктором:
Команда Spring главным образом выступает за инъекцию через конструктор, поскольку она позволяет реализовывать компоненты приложения как неизменяемые объекты и гарантировать, что требуемые зависимости не null. Более того, компоненты, внедренные через через конструктор, всегда возвращаются в клиентский код в полностью инициализированном состоянии. Как небольшое замечание, большое число аргументов конструктора является признаком «кода с запашком» и подразумевает, что у класса, вероятно, слишком много обязанностей, и его необходимо реорганизовать, чтобы лучше решать вопрос о разделении ответственности.

Инъекция через сеттер должна использоваться в первую очередь для опциональных зависимостей, которым могут быть присвоены значения по-умолчанию внутри класса. В противном случае, проверки на not-null должны быть использованы везде, где код использует эти зависимости. Одно из преимуществ использования внедрения через сеттеры заключается в том, что они делают объекты класса поддающимися реконфигурации и повторному инжектированию позже

Заключение


В основном стоит избегать внедрения через поля. Как альтернативу для внедрения следует использовать сеттеры или конструкторы. У каждого из них есть свои преимущества и недостатки в зависимости от ситуации. Однако так как эти подходы можно смешивать, это не выбор «или-или» и вы можете в одном классе комбинировать инъекцию и через сеттер, и через конструктор. Конструкторы больше подходят для обязательных зависимостей и при нужде в неизменяемых объектах. Сеттеры лучше подходят для опциональных зависимостей.
Поделиться с друзьями
-->

Комментарии (59)


  1. grossws
    01.08.2017 15:25

    Одна из ключевых идей DI-фреймворков заключается в том, что управляемый класс не должен зависеть от конкретного используемого контейнера.… snip… Если нет завязки на контейнер, вы можете использовать класс как управляемый или неуправляемый, или даже переключиться на другой DI-фреймворк.

    При этом, в реальности это всё равно не так. Вы можете менять контейнер, например, в пределах разных имплементаций CDI, но для перескакивания на Guice или Spring часто придётся править аннотации в этих классах. Spring, вроде, имел некоторый уровень совместимости с javax.inject, но умел далеко не всё. Как сейчас — не знаю.


    1. grossws
      01.08.2017 15:33

      Upd: дочитал статью и увидел, что спринговцы "починили" кусок: неявное внедрение через конструктов (без использования @Autowired), т. е. если класс имеет ровно один конструктор, то он может быть не завязан на classpath spring'а.


  1. tmn4jq
    01.08.2017 15:43
    +3

    Но суть же не в том, что инъекция через поля — это зло, скорее злом является чрезмерно сложный класс. А если класс лаконичен и не требует рефакторинга, то будет ли проблемой заинжектить 1-2 поля через Autowired?


    1. mird
      01.08.2017 15:49
      +1

      А в тесте вы как моки будете инжектить в эти поля? DI, он не в последнюю очередь для упрощения тестирования, а когда вы инжектити зависимость в приватное поле — это почти тоже самое, что создать там зависимость через new.


      1. tmn4jq
        01.08.2017 15:55
        +1

        А в чем проблема заинжектить моки в поля? InjectMocks, MockitoAnnotations.initMocks(). Те же моки уйдут в тестовый бин как через поля, так и через сеттеры


        1. tmn4jq
          01.08.2017 16:08
          +2

          Поправочка: проблема в том, что это будет требовать запуска DI-контейнера, что не всегда нужно.


      1. sentyaev
        02.08.2017 21:23
        -1

        Сделайте поле package private, тесты же обччно в том же пакете. И все будет ок.
        Я в своем C# просто мечтаю о таком.


        1. DrFdooch
          02.08.2017 22:51

          internal и [assembly:InternalsVisibleTo(''Project.Tests'')]?


        1. fogone
          03.08.2017 08:07

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


    1. mayorovp
      01.08.2017 16:01
      +5

      Инжект в приватное поле "привязывает" класс к IoC-контейнеру — объект такого класса нельзя создать иначе чем с его помощью; при обычном создании через new он просто не будет работать. Наличие "магии" — само по себе не плохо, плохо когда "магия" становится обязательной.


      В то же время, если класс является точкой входа в пользовательский код и принципиально создается только кодом платформы — то в инъекции через поля нет ничего плохого. Не знаю как в мире java, но в ASP.NET MVC такими классами являются виды и контроллеры — для них строгая инъекция через конструктор избыточна (а для видов — и вовсе невозможна).


      1. denismaster
        02.08.2017 10:20

        Что в .NET, что в .NET Core общепринято использовать внедрение через конструктор в контроллерах. А если необходимо внедрить много зависимостей, можно их обернуть в фасад и внедрять его, тем самым вынося всю логику в сервисные классы. Тогда контроллеры получаются легкими. Опциональные свойства можно также внедрять внутри сервисных классов.
        А касательно видов, зачем там внедрение?) Достаточно ведь ViewModel.


        1. mayorovp
          02.08.2017 11:00

          Ну например, лично мне не нравится практика загрузки всех классификаторов и справочников в контроллере с последующей передачей через ViewModel — слишком много boilerplate-кода получается, в каждом методе дублируется одно и то же. Гораздо веселее получается если разрешить виду запрашивать подобную информацию у модели напрямую.


          1. denismaster
            02.08.2017 11:07

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


            1. mayorovp
              02.08.2017 11:13

              А какой логике речь? Логика там простая: все записи справочника должны, к примеру, стать тэгами option внутри select :-)


              1. denismaster
                02.08.2017 11:20

                можно внутри сервиса сразу переводить их в формат, удобный для представления.

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


                1. mayorovp
                  02.08.2017 11:23

                  Ну вот переведете вы их в формат, удобный для представления (кстати, что это? SelectListItem достаточно удобный или вы предпочитаете в сервисе сразу разметку генерировать?). Как теперь их в вид из сервиса передать-то?


                  1. denismaster
                    02.08.2017 11:36

                    Да, я имел в виду SelectListItem. Впрочем, иногда могут быть и другие модели представления.

                    Что касается второго вопроса:

                    public class MyController: Controller
                    {
                      private readonly IMyService _myService;
                    
                      public MyController(IMyService myService)
                      {
                          if(myService==null) throw new ArgumentNullException(nameof(myService));
                          _myService = myService;
                      }
                      ...
                      public IEnumerable<SelectListItem> GetSelectValues()
                      {
                        ViewBag.SelectItems = _myService.GetSelectItems();
                        return View();
                      }
                      .... // Вариант 2 
                       public IEnumerable<SelectListItem> GetSelectValues2()
                      {
                        var viewModel = _myService.GetSelectItems();
                        return View(viewModel);
                      }
                    }
                    


                    public class MyService: IMyService
                    {
                      private readonly IRepository<Classificator> _repository;
                    
                      ...//инжектим аналогично репозиторий
                      ...
                      public IEnumerable<SelectListItem> GetSelectItems()
                      {
                         var classificators = _repository.GetAll();
                         return classificators.Select(c=> {
                             return new SelectListItem(){ Text = c.Name, Value = c.Value };
                         }
                      }
                    }
                    


                    1. mayorovp
                      02.08.2017 11:48

                      Ну например, лично мне не нравится практика загрузки всех классификаторов и справочников в контроллере с последующей передачей через ViewModel — слишком много boilerplate-кода получается, в каждом методе дублируется одно и то же. Гораздо веселее получается если разрешить виду запрашивать подобную информацию у модели напрямую.


                      1. denismaster
                        02.08.2017 11:56

                        Для устранения дублирования маппинга разумно использовать automapper.
                        Тогда код сервиса станет таким:

                        public class MyService: IMyService
                        {
                          private readonly IRepository<Classificator> _repository;
                          private readonly IMapper _mapper;
                        
                          ...//инжектим аналогично репозиторий
                          ...
                          public IEnumerable<SelectListItem> GetSelectItems()
                          {
                             var classificators = _repository.GetAll();
                             return _mapper.Map<SelectListItem>(classificators);
                          }
                        }
                        


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

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


                        1. mayorovp
                          02.08.2017 12:03

                          Мне надоело писать и видеть в каждом контроллере вот эти строчки:


                          vm.FooTypes = _fooTypesService.GetAllTypes();
                          vm.BarKinds = _barKindsService.GetAllKinds();
                          vm.BazObjects = _bazObjectsService.GetAllObjects();
                          // и еще 10 штук

                          Да при чем тут маппинг или логика? Где вы тут их нашли?


                          1. denismaster
                            02.08.2017 12:10

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

                            public class FooBarBazFacadeService
                            {
                              private readonly FooTypesService _fooTypesService;
                              private readonly BarKindsService _barKindsService; 
                              private readonly BazObjectsService _bazObjectsService; 
                              //.. и еще 10 штук
                            
                              public Model GetViewModel()
                              {
                                 var vm= new Model();
                                 vm.FooTypes = _fooTypesService.GetAllTypes();
                                 vm.BarKinds = _barKindsService.GetAllKinds();
                                 vm.BazObjects = _bazObjectsService.GetAllObjects();
                                 //Заполняете тут вообщем все что нужно
                                 return vm;
                              }
                            }
                            

                            И внедряете в контроллер один сервис, вместо кучи однотипных.


                            1. mayorovp
                              02.08.2017 13:02

                              То есть вы предлагаете на каждый контроллер создавать по фасаду? Не вижу преимуществ.


                              1. denismaster
                                02.08.2017 13:12

                                Вы сами сказали:

                                Мне надоело писать и видеть в каждом контроллере вот эти строчки:

                                vm.FooTypes = _fooTypesService.GetAllTypes();
                                vm.BarKinds = _barKindsService.GetAllKinds();
                                vm.BazObjects = _bazObjectsService.GetAllObjects();
                                // и еще 10 штук
                                


                                Из этого следует, что у вас несколько контроллеров, при этом эти строчки кода одинаковые в каждом из контроллеров. Поэтому я и предложил вынести код в фасад и внедрять его. Он также может еще и внедрять другой сервис какой-нибудь. Таким образом, вы выносите общий код в отдельный фасад(тем самым у вас все по DRY), вы можете его использовать во всех контроллерах где хотите, а еще у вас представление не загрязнено бизнес-логикой.


                                1. mayorovp
                                  02.08.2017 13:14

                                  Я все еще не понимаю, почему вы вызов _fooTypesService.GetAllTypes() называете бизнес-логикой. И почему вы решили что если эти строчки есть в трех контроллерах, то и в четвертом каждая из них тоже понадобится.


                                  1. denismaster
                                    02.08.2017 13:23

                                    Я думаю их использование в 3 контроллерах уже очевидный повод вынести код, а не бездумно копипастить. Как вариант я предложил фасад.
                                    А называю я его бизнес-логикой, потому что скорее всего вы получаете эти данные из БД, а в ходе развития приложения способы получения данных усложняются. Возможно, вам потом придется каким-либо образом фильтровать эти самые данные, или сортировать, или еще допустим рассчитывать на их основе другие какие-то значения, и так далее.


                                    1. mayorovp
                                      02.08.2017 13:30

                                      Конкретно в приведенном мною коде никакого получения данных из БД нет — этим занимается реализация сервиса.


                                      По поводу же вынесения кода… Пусть у нас есть 4 вида (и контроллера к ним) и 4 источника справочных данных. Первому виду нужны все справочники кроме четвертого, второму — все кроме третьего, третьему — все кроме второго и четвертому — все кроме первого.


                                      Что именно вы будете в такой ситуации выносить в фасад?


                                      1. denismaster
                                        02.08.2017 13:50

                                        Создам один класс-фасад, который будет внедрять все 4 сервиса данных, а также несколько методов(или даже один с параметрами) для получения именно нужных данных.
                                        Фасад здесь применим, поскольку он может логически отделить работу со справочниками от остального кода. Также он может предоставлять доступ к внедренным сервисам.


                                        1. mayorovp
                                          02.08.2017 20:59

                                          А методов в фасаде будет столько же, сколько видов?


                                          1. denismaster
                                            03.08.2017 09:37

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

                                            var viewModel = _service.Use(modelToProceed).WithFoo().WithBar().Proceed();
                                            

                                            Это просто пример, вариантов масса.


                                            1. mayorovp
                                              03.08.2017 09:38

                                              Ужас.


                                              1. denismaster
                                                03.08.2017 10:07
                                                -1

                                                Я просто привел пример, как еще можно сделать, что ужасного в паттерне Builder? Можно и одним методом, например

                                                public ViewModel FillViewModel(bool useFoo, bool useBar, bool useBaz)
                                                {
                                                   var vm = new ViewModel();
                                                   if(useFoo) 
                                                      vm.FooThings = _fooService.GetAll();
                                                   if(useBar) 
                                                      vm.BarClasses = _barService.GetAll();
                                                   if(useBaz) 
                                                      vm.BazObjects = _bazService.GetAll();
                                                }
                                                ...
                                                var viewModel = _service.FillViewModel(true,false, true);
                                                

                                                Выносить логику на сторону представления — попахивающий код. А сервисы можно поддерживать.


      1. polly5315
        03.08.2017 11:00

        Для видов возможна инъекция через конструктор, но делается она немного нетривиально. Для начала нужно создать класс (желательно абстрактный), отнаследованный от базового класса вида (сейчас не могу сказать точно, какой это класс, возможно, WebViewPage). В этом абстрактном классе вы реализуете конструктор с зависимостями, и в cshtml-коде вида указываете его через @inherit. Реальный класс вида, который будет использоваться приложением будет создан на лету на основе Razor-кода, и будет отнаследован от вашего класса, и будет иметь все нужные зависимости, которые внедрит, допустим Autofac, или какйо у вас там используется IoC.


        1. denismaster
          03.08.2017 11:20
          -1

          Но разве внедрение в представление не считается плохой практикой? Если нет, то почему?


          1. polly5315
            03.08.2017 12:58

            Нет, не является. Вообще я считаю хорошей практикой, когда контроллер выбирает, какой вид нужно показать и передает ему минимальный нужный набор опций для показа (то есть не большие сложные так называемые «вью-модели», а что-то маленькое). И после этого представление исходя из этих данных и своих зависимостей должно собрать страницу. То есть если у нас есть какой-нибудь userId, а представление должно показать пользователя и множество связанных с ним объектов, то не стоит собирать их все в коде контроллера, собирать в один сложный объект и передавать во View, достаточно передать только userId, а представление само обратится ко всем инжектированным в него репозиториям и покажет то, что надо.


            1. polly5315
              03.08.2017 13:14

              Хм… да, я не ответил на вопрос «почему»: потому что это соответствует SRP, и переносит ответственность вида из контроллера в вид. Если ему для выполнения ответственности нужны какие-то зависимости, их нужно в него внедрить.


              1. denismaster
                03.08.2017 13:29

                Странно, просто обычно люди заявляют о другом. У меня похожие мысли. Представление не должно обладать логикой, разве что в крайне необходимых случаях. Страница не должна собирать сама себя.
                Можно использовать сервисы, которые будут создавать нужные ViewModel, оставляя за контроллером лишь обязанность по делегированию запросов сервисам и обратно. Либо можно разделить ViewModel на несколько сущностей и подгружать их асинхронно.
                Получение же данных самим видом переносит логику получения данных с контроллера на вид. Таким образом модель кроме выполнения своей обязанности(обновления состояния), еще и начинает получать данные, что противоречит как раз таки SRP и самому паттерну MVC.
                Нет, я не спорю, внедрение в вид возможно для всяких сервисов-утилит, необходимых для непосредственного отображения. Но не в других случаях и тем более не для получения данных.


                1. polly5315
                  03.08.2017 14:42
                  +2

                  Я вас понимаю, но рискну не согласиться. Обычно так оно делается, да, но вы не подумайте, что я имею в виду, что из вида нужно изменять состояние приложения, нет. Но получать данные о состоянии — вполне можно, и это не противоречит MVC. Смотрите: действие контроллера — это конечная точка маршрутизации. Здесь код максимально простой: оповещение бизнес-логики о том, что что-то произошло и возврат вида. Контроллер буквально говорит виду: «Покажи пользователя с таким-то идентификатором», и все. Вид его показывает. Если нужно передать еще какие-то данные, стоит передать, но не все те мелочи, которые представление собирается всавить в свой шаблон. Только ключевое. Потому что иначе контроллер (или провайдер ViewModel, которым он пользуется) становится жестко зависим от реализации представления.


                1. polly5315
                  03.08.2017 14:48
                  +1

                  Попробуйте сами себе ответить, зачем вообще нужны большие странные POCO-ViewModels? Есть ли у них свой смысл? В них всегда столько разномастной информации. Если бы View мог получать недостающие нужные ему данные, этих классов бы вообще не было в большинстве случаев.


                  1. denismaster
                    03.08.2017 14:57

                    Если View сам получает данные, это нарушает Separation of Concerns.
                    Контроллер пусть говорит не «Покажи пользователя с таким то идентификатором», а «покажи пользователя, вот он, и вот его дополнительный багаж!».
                    Согласен, есть проблема больших разномастных POCO, но с другой стороны, можно использовать анонимные объекты, кортежи, чтобы не писать определение нового POCO, а можно использовать ajax и подгружать данные по ходу. Можно использовать GraphQL и за один запрос получать нужные данные, а сервис там сам разберется, что отдать клиенту, как и представление — как отобразить.


                    1. mayorovp
                      03.08.2017 15:03

                      Какое именно разделение ответственности нарушается запросом от вида к модели?


                    1. polly5315
                      03.08.2017 15:45
                      +1

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


                      1. denismaster
                        03.08.2017 16:01

                        Вопрос не в том, будет ли это работать или нет, вопрос вообще лежит в другой плоскости. Согласно паттерну MVC, возможно менять само представление, при этом контроллер и модель не затрагиваются. Например, поменять список на график. В случае же, когда представление само запрашивает данные в обход контроллера, это не гарантируется. Невозможно будет заменить представление не зная особенностей логики получения этих данных. Нельзя будет отдать верстальщику и сказать, мол, сюда и сюда вставляй такое, а сюда и сюда — такое. Усложняется само представление, когда у нас нет данных изначально, а их еще надо получить.
                        Также теряется роль контроллера, если представление сможет само дергать данные из модели(сервис это слой модели).
                        А с другой стороны, также невозможно будет реализовать такое представление, когда в виде View у нас SPA или мобильное приложение.
                        А в случае генерации вью-моделей на стороне сервиса и передачей их через контроллер мы поддерживаем будущий переход на SPA или мобильные приложение, поддерживаем простоту представления, поддерживает правильное разделение зон ответственности и обязанностей.
                        Вы сами сказали, что

                        оно пользуется своим инжектированным интерфейсом

                        Почему представление должно вообще использовать сервисы, если оно может использовать контроллер, который по сути тоже реализует контракт? Тем самым мы вновь возвращаемся к генерации вью-моделей или использованию ajax/rest/graphql


                        1. polly5315
                          03.08.2017 16:54

                          Потому что представление знает о модели. Это прямо описывается в паттерне MVC. В этой фразе под словом «модель» подразумевается не POCO, представляющий какую-то сущность, передаваемую обычно из контроллера в модель, а «бизнес-модель». То есть оно вполне вправе запросить какие-то недостающие данные. Поищите, что пишет Мартин Фаулер об MVC, можно даже просто зайти на Википедию, там говорится: «Представление отвечает за получение необходимых данных из модели и отправляет их пользователю.» Уверяю, нет никаких причин запрещать представленю делать неизменяющие запросы к сервисам.
                          И это не помешает верстальщикам. Получение данных происходит в абстрактном классе, а Razor-шаблон в cshtml просто пользуется уже тем, что есть в его родительском классе. Ведь этот шаблон по сути просто такой язык, из которого собирается код одного метода рендера, с которым создается представление-наследник.


                          1. denismaster
                            03.08.2017 17:27

                            Представление и работает с моделью — через контроллер, иначе зачем контроллер?
                            А если нужен контроллер, то зачем городить костыли через внедрение в представление, когда можно воспользоваться PartialView, AJAX-запросами и так далее? Это обеспечит также поддержку и мобильных устройств с разными SPA.


                            1. polly5315
                              03.08.2017 17:40

                              Контроллер — это конечная точка маршрутизации запроса. Его роль только сообщить бизнес-модели, что что-то произошло (как раз тот изменяющий состояние вызов, который нельзя делать из представления), и возврат представления. Он не должен заниматься сбором особенных данных для конкретно этого представления. Он должен сказать:
                              — Бизнес-логика, тут хотят создать нового пользователя! Оп, спасибо за идентификатор.
                              — Представление, покажи пользователя!
                              И ему нет разницы, какое это представление сложное, что ему нужно о пользователе знать его баланс, подписчиков или особенности его ролей.
                              Если он будет это все делать, он будет превращаться в ТУУК.


                              1. denismaster
                                03.08.2017 17:52

                                Согласно вашей же логике, вот как работает все:

                                — Бизнес-логика, тут хотят создать нового пользователя! Оп, спасибо за идентификатор.
                                — Представление, покажи пользователя по идентификатору!

                                — Бизнес-логика, дай пользователя с этим идентификатором!
                                — Без проблем, представление! Держи!


                                И только после этого у нас идет отображение.
                                Я же предлагаю вариант с меньшим числом потоков данных.

                                — Бизнес логика, тут хотят создать нового пользователя! Оп, спасибо за него! Вьюха, лови!

                                Все. И ничего больше.


                                1. polly5315
                                  03.08.2017 18:02

                                  Конечно, так тоже можно, но если представление хочет кроме этого еще много всего, и контроллеру приходится собирать для него все дополнительные данные, паковать во POCO-вьюмодель, вот этого как раз и можно избежать.
                                  Например, завтра нам понадобилось в представлении показывать еще и последние пять постов пользователя. Не придется изменять код контроллера, чтобы их показать, только код представления.


                                  1. denismaster
                                    03.08.2017 19:07

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


        1. mayorovp
          03.08.2017 11:21

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


        1. polly5315
          03.08.2017 17:17

          И тут я понял, что я забылся: по поводу наследования вида и всего этого с Razor я писал в контексте ASP.NET MVC, а тут речь про Java. Прошу прощения. Но я думаю, что там все-таки есть какой-то похожий способ внедрения в представление через конструктор.


          1. mayorovp
            04.08.2017 13:35

            Да нет, тут речь именно про параллельную вселенную шла :-)


            Не знаю как в мире java, но в ASP.NET MVC такими классами являются виды и контроллеры — для них строгая инъекция через конструктор избыточна (а для видов — и вовсе невозможна).


    1. DrFdooch
      01.08.2017 17:37
      +1

      Слышал такую контраргументацию пару раз. Но даже самые ответственные разработчики делают монстров, когда это незаметно, а что уж говорить о неокрепших умах… для которых такие статьи и должны нести свет истины. Достаточно хотя бы в уголочке одним предложеньицем курсивом написать «но ___иногда___ можно» и остальная статья отправляется в сознании читающего в небытие, ведь «мой-то случай как раз особенный». И имеем в итоге то, что имеем.


  1. saroff
    01.08.2017 16:43
    +2

    Нарушение принципа единственной ответственности

    Каким образом инъекция через поля нарушает этот принцип? Его нарушение становится не так легко заметить — да, но в его нарушении поля никак не виноваты.


    1. osigida
      01.08.2017 22:44
      +1

      статья как бы не делает таких резких заявлений:

      Наличие слишком большого количества зависимостей обычно означает, что у класса слишком много зон ответственности. Это может быть нарушением принципов единственной ответственности (single responsibility) и разделения ответственности (ориг.: separation of concerns)


      может быть, а может и не быть, но код явно начинает попахивать.


  1. SamSol
    02.08.2017 02:40
    +1

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


    1)


    Добавлять новые зависимости просто.

    При использовании Idea любую зависимость добавлять легко, так что это не аргумент.
    Кроме того нет смысла что-то усложнять.


    2)


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

    Тем не менее это не ведет к улучшению архитектуры. Коммерческие разработчики под прессом дедлайнов тупо добавляют еще один аргумент в конструктор. Благо с Идеей это легко. Когда же разработчики берутся за рефакторинг, то они выбирают классы не по числу аргументов конструкторов, а по размеру или сложности класса. Так что и это не аргумент.


    3)


    Таким образом становится четко понятно, что требует класс...

    Кому? DI контейнер однозначно "поймет", что требует класс (а точнее бин!) при любом объявлении зависимостей.
    Разработчику зависимых бинов совершенно без разницы от чего зависит ваш бин. При написании юнит-тестов разработчику доступен исходный код класса — так что, чем меньше кода — тем лучше.


    4)


    … а также опциональные ли это зависимости (через сеттеры) или обязательные (конструктор)

    Увы это ложь.
    Например:


    @Autowired public void setTest(AI ai) {} // Обязательная зависимость

    Если в конфигурации не будет реализации интерфейса AI, spring выбросит UnsatisfiedDependencyException, при создании такого бина.
    Чтобы получить не обязательную зависимость можно использовать, например:


    @Autowired(required = false) public void setTest(AI ai) {} // Необязательна зависимость
    @Autowired public void setTest(Optional<AI> ai) {} // Необязательна зависимость
    @Autowired public void setTest(javax.enterprise.inject.Instance<AI> ai) {} // Разрешение зависимости под вашим контролем

    5)


    … вы можете создать его в юнит-тесте без запуска контейнера

    Использование подходящих инструментов делает юнит тесты простыми.
    Например этот тест будет работать без контейнера и при любом способе внедрения зависимостей (конструктор, поля, сеттеры, с аннотациям @Autowired, Inject или вообще без них):


    @RunWith(MockitoJUnitRunner.class)
    public class SmartCarTest {
    
        @InjectMocks
        SmartCar smartCar;
    
        @Mock
        AI ai;
    
        @Test
        public void testDecide() {
            when(ai.think()).thenReturn("42");
    
            String decision = smartCar.decide();
    
            assertEquals("The answer is: 42", decision);
        }
    }

    6)


    Существует способ… создать объект… приведет к NullPointerException

    Это опять ложь.
    В рантайме спринг либо создаст объект со всеми обязательными зависимостями, либо выбросит UnsatisfiedDependencyException. CDI или другие DI контейнеры — я уверен — ведут себя также. То же самое верно и для тестов с использованием SpringRunner.
    А для тестов с использованием Mockito NullPointerException не является проблемой. Это будет просто упавший тест.


    7)


    … нет способа кроме рефлексии предоставить ему необходимые зависимости

    Для DI контейнера это не проблема. А если класс нужно использовать вне DI контейнера, то нет смысла говорить о внедрении зависимостей. Так что это не аргумент против любого из способов.


    8)


    … внедрение через поля не может использоваться для присвоения зависимостей final-полям

    Это верно не для всех DI контейнеров.
    CDI реализации в серверах Wildfly и Payara, насколько я помню, вполне комфортно внедряют такие зависимости:


    class SmartCar {
        @Inject
        final AI ai = null;
    }


  1. Lioshik
    02.08.2017 11:32

    Если мы говорим про Spring, то не надо забывать про scope бинов. При использовании не singleton scope (prototype, request,session.....) внедрение самого бина в конструкторе невозможно. И тут уже либо сеттеры либо поля.


    1. fogone
      02.08.2017 19:32
      +2

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


  1. Nakosika
    02.08.2017 14:58

    Если вы имеете мутабельные зависимости и используете что-то кроме передачи их через конструктор, то что-то вы делаете явно не то.


  1. rraderio
    07.08.2017 11:54

    Как можно наблюдать, вариант внедрения через поле выглядит очень привлекательным. Он очень лаконичен, выразителен, отсутствует шаблонный код. По коду легко перемещаться и читать его. Ваш класс может просто сфокусироваться на основной функциональности и не загромождается шаблонным DI-кодом. Вы просто помещаете аннотацию @Autowired над полем — и все. Не надо писать специальных конструкторов или сеттеров только для того, чтобы DI-контейнер предоставил необходимые зависимости. Java довольно многословна сама по себе, так что стоит использовать любую возможность, чтобы сделать код короче, верно?

    Kotlin
    @RestController
    class UsersController(private val request: HttpServletRequest) {
        // ...
    }
    

    Выглядит как через поле, но внедряем через конструктор