Комментарии 59
Одна из ключевых идей DI-фреймворков заключается в том, что управляемый класс не должен зависеть от конкретного используемого контейнера.… snip… Если нет завязки на контейнер, вы можете использовать класс как управляемый или неуправляемый, или даже переключиться на другой DI-фреймворк.
При этом, в реальности это всё равно не так. Вы можете менять контейнер, например, в пределах разных имплементаций CDI, но для перескакивания на Guice или Spring часто придётся править аннотации в этих классах. Spring, вроде, имел некоторый уровень совместимости с javax.inject
, но умел далеко не всё. Как сейчас — не знаю.
Инжект в приватное поле "привязывает" класс к IoC-контейнеру — объект такого класса нельзя создать иначе чем с его помощью; при обычном создании через new он просто не будет работать. Наличие "магии" — само по себе не плохо, плохо когда "магия" становится обязательной.
В то же время, если класс является точкой входа в пользовательский код и принципиально создается только кодом платформы — то в инъекции через поля нет ничего плохого. Не знаю как в мире java, но в ASP.NET MVC такими классами являются виды и контроллеры — для них строгая инъекция через конструктор избыточна (а для видов — и вовсе невозможна).
А касательно видов, зачем там внедрение?) Достаточно ведь ViewModel.
Ну например, лично мне не нравится практика загрузки всех классификаторов и справочников в контроллере с последующей передачей через ViewModel — слишком много boilerplate-кода получается, в каждом методе дублируется одно и то же. Гораздо веселее получается если разрешить виду запрашивать подобную информацию у модели напрямую.
Я считаю плохой практикой реализовывать сложную логику в представлении.
А какой логике речь? Логика там простая: все записи справочника должны, к примеру, стать тэгами option внутри select :-)
В моих проектах контроллеры просто делегируют вызовы сервисам. Сервисы уже работают с репозиториями, таск-раннером и прочим. При этом сервисы обычно сразу возвращают готовые модели представления. При этом логика маппинга моделей на модели представления вынесена отдельно в другие классы или конфиг AutoMapper
Ну вот переведете вы их в формат, удобный для представления (кстати, что это? 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 };
}
}
}
Ну например, лично мне не нравится практика загрузки всех классификаторов и справочников в контроллере с последующей передачей через ViewModel — слишком много boilerplate-кода получается, в каждом методе дублируется одно и то же. Гораздо веселее получается если разрешить виду запрашивать подобную информацию у модели напрямую.
Тогда код сервиса станет таким:
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);
}
}
Вы ведь все равно прокидываете модель во вью, а тут можно при этом управлять и логикой вывода, и навешивать логгинг и дополнительные параметры работы, и настраивать маппинг, и при этом вью остается чистым и красивым.
Конечно, у каждого свои любимые фломастеры, но на мой взгляд, в представлении не должно быть сложной логики и маппинга.
Мне надоело писать и видеть в каждом контроллере вот эти строчки:
vm.FooTypes = _fooTypesService.GetAllTypes();
vm.BarKinds = _barKindsService.GetAllKinds();
vm.BazObjects = _bazObjectsService.GetAllObjects();
// и еще 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;
}
}
И внедряете в контроллер один сервис, вместо кучи однотипных.
То есть вы предлагаете на каждый контроллер создавать по фасаду? Не вижу преимуществ.
Мне надоело писать и видеть в каждом контроллере вот эти строчки:
vm.FooTypes = _fooTypesService.GetAllTypes(); vm.BarKinds = _barKindsService.GetAllKinds(); vm.BazObjects = _bazObjectsService.GetAllObjects(); // и еще 10 штук
Из этого следует, что у вас несколько контроллеров, при этом эти строчки кода одинаковые в каждом из контроллеров. Поэтому я и предложил вынести код в фасад и внедрять его. Он также может еще и внедрять другой сервис какой-нибудь. Таким образом, вы выносите общий код в отдельный фасад(тем самым у вас все по DRY), вы можете его использовать во всех контроллерах где хотите, а еще у вас представление не загрязнено бизнес-логикой.
Я все еще не понимаю, почему вы вызов _fooTypesService.GetAllTypes()
называете бизнес-логикой. И почему вы решили что если эти строчки есть в трех контроллерах, то и в четвертом каждая из них тоже понадобится.
А называю я его бизнес-логикой, потому что скорее всего вы получаете эти данные из БД, а в ходе развития приложения способы получения данных усложняются. Возможно, вам потом придется каким-либо образом фильтровать эти самые данные, или сортировать, или еще допустим рассчитывать на их основе другие какие-то значения, и так далее.
Конкретно в приведенном мною коде никакого получения данных из БД нет — этим занимается реализация сервиса.
По поводу же вынесения кода… Пусть у нас есть 4 вида (и контроллера к ним) и 4 источника справочных данных. Первому виду нужны все справочники кроме четвертого, второму — все кроме третьего, третьему — все кроме второго и четвертому — все кроме первого.
Что именно вы будете в такой ситуации выносить в фасад?
Фасад здесь применим, поскольку он может логически отделить работу со справочниками от остального кода. Также он может предоставлять доступ к внедренным сервисам.
А методов в фасаде будет столько же, сколько видов?
var viewModel = _service.Use(modelToProceed).WithFoo().WithBar().Proceed();
Это просто пример, вариантов масса.
Ужас.
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);
Выносить логику на сторону представления — попахивающий код. А сервисы можно поддерживать.
Можно использовать сервисы, которые будут создавать нужные ViewModel, оставляя за контроллером лишь обязанность по делегированию запросов сервисам и обратно. Либо можно разделить ViewModel на несколько сущностей и подгружать их асинхронно.
Получение же данных самим видом переносит логику получения данных с контроллера на вид. Таким образом модель кроме выполнения своей обязанности(обновления состояния), еще и начинает получать данные, что противоречит как раз таки SRP и самому паттерну MVC.
Нет, я не спорю, внедрение в вид возможно для всяких сервисов-утилит, необходимых для непосредственного отображения. Но не в других случаях и тем более не для получения данных.
Контроллер пусть говорит не «Покажи пользователя с таким то идентификатором», а «покажи пользователя, вот он, и вот его дополнительный багаж!».
Согласен, есть проблема больших разномастных POCO, но с другой стороны, можно использовать анонимные объекты, кортежи, чтобы не писать определение нового POCO, а можно использовать ajax и подгружать данные по ходу. Можно использовать GraphQL и за один запрос получать нужные данные, а сервис там сам разберется, что отдать клиенту, как и представление — как отобразить.
Какое именно разделение ответственности нарушается запросом от вида к модели?
Также теряется роль контроллера, если представление сможет само дергать данные из модели(сервис это слой модели).
А с другой стороны, также невозможно будет реализовать такое представление, когда в виде View у нас SPA или мобильное приложение.
А в случае генерации вью-моделей на стороне сервиса и передачей их через контроллер мы поддерживаем будущий переход на SPA или мобильные приложение, поддерживаем простоту представления, поддерживает правильное разделение зон ответственности и обязанностей.
Вы сами сказали, что
оно пользуется своим инжектированным интерфейсом
Почему представление должно вообще использовать сервисы, если оно может использовать контроллер, который по сути тоже реализует контракт? Тем самым мы вновь возвращаемся к генерации вью-моделей или использованию ajax/rest/graphql
И это не помешает верстальщикам. Получение данных происходит в абстрактном классе, а Razor-шаблон в cshtml просто пользуется уже тем, что есть в его родительском классе. Ведь этот шаблон по сути просто такой язык, из которого собирается код одного метода рендера, с которым создается представление-наследник.
А если нужен контроллер, то зачем городить костыли через внедрение в представление, когда можно воспользоваться PartialView, AJAX-запросами и так далее? Это обеспечит также поддержку и мобильных устройств с разными SPA.
— Бизнес-логика, тут хотят создать нового пользователя! Оп, спасибо за идентификатор.
— Представление, покажи пользователя!
И ему нет разницы, какое это представление сложное, что ему нужно о пользователе знать его баланс, подписчиков или особенности его ролей.
Если он будет это все делать, он будет превращаться в ТУУК.
— Бизнес-логика, тут хотят создать нового пользователя! Оп, спасибо за идентификатор.
— Представление, покажи пользователя по идентификатору!
— Бизнес-логика, дай пользователя с этим идентификатором!
— Без проблем, представление! Держи!
И только после этого у нас идет отображение.
Я же предлагаю вариант с меньшим числом потоков данных.
— Бизнес логика, тут хотят создать нового пользователя! Оп, спасибо за него! Вьюха, лови!
Все. И ничего больше.
Например, завтра нам понадобилось в представлении показывать еще и последние пять постов пользователя. Не придется изменять код контроллера, чтобы их показать, только код представления.
Кажется, прошлый раз у меня не получалось так сделать, но причину не помню. Когда проверю — напишу подробнее.
Нарушение принципа единственной ответственности
Каким образом инъекция через поля нарушает этот принцип? Его нарушение становится не так легко заметить — да, но в его нарушении поля никак не виноваты.
Наличие слишком большого количества зависимостей обычно означает, что у класса слишком много зон ответственности. Это может быть нарушением принципов единственной ответственности (single responsibility) и разделения ответственности (ориг.: separation of concerns)
может быть, а может и не быть, но код явно начинает попахивать.
К сожалению автор оригинальной статьи не удосужился проверить свои аргументы.
А между тем, эти аргументы слабенькие, а некоторые — ложные.
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;
}
Если вы имеете мутабельные зависимости и используете что-то кроме передачи их через конструктор, то что-то вы делаете явно не то.
Как можно наблюдать, вариант внедрения через поле выглядит очень привлекательным. Он очень лаконичен, выразителен, отсутствует шаблонный код. По коду легко перемещаться и читать его. Ваш класс может просто сфокусироваться на основной функциональности и не загромождается шаблонным DI-кодом. Вы просто помещаете аннотацию @Autowired над полем — и все. Не надо писать специальных конструкторов или сеттеров только для того, чтобы DI-контейнер предоставил необходимые зависимости. Java довольно многословна сама по себе, так что стоит использовать любую возможность, чтобы сделать код короче, верно?
Kotlin
@RestController
class UsersController(private val request: HttpServletRequest) {
// ...
}
Выглядит как через поле, но внедряем через конструктор
Внедрение зависимостей через поля — плохая практика