Spring — мощный Java-фреймворк, доступный в опенсорсе. Я решил рассказать, для каких задач на бэкенде Spring оказывается полезным и какие у него плюсы-минусы в сравнении с другими библиотеками: Guice и Dagger 2. Рассмотрим инъекцию зависимостей и инверсию управления — вы узнаете, с чего лучше начать изучение этих принципов.
— Всем привет, меня зовут Кирилл. Я буду сегодня рассказывать про Dependency Injection.
Мы начнем с того, как у меня называется доклад. «В некотором царстве, не в «спринговом» государстве». Мы поговорим, конечно, про Spring, но также хочется посмотреть на все, что есть помимо него. О чем конкретно поговорим?
![](https://habrastorage.org/r/w780q1/webt/cq/li/gv/cqligvmc0hnuu56z8mgqjjnoebo.jpeg)
Я сделаю небольшое отступление — расскажу, над чем я работаю, какой у меня проект, зачем мы используем Dependency Injection. Потом расскажу, что это вообще такое, сравню Inversion of Control и Dependency Injection, и расскажу о его реализации в трех самых известных библиотеках.
Я работаю в команде Яндекс.Трекера. Мы делаем продуктовый аналог Jira или Trello. [...] Мы решили сделать собственный продукт, который сначала был внутренним. Сейчас мы его продаем наружу. Каждый из вас может зайти, создать себе собственный Трекер и делать задачи — например, учебные или бизнесовые.
Посмотрим на интерфейс. В примерах я буду использовать некоторые термины из моей области. Мы попробуем создать тикет и в нем посмотрим на комментарии, которые будут мне оставлять другие коллеги.
Начну с того, что вообще такое Dependency Injection. Это такой паттерн программирования, который отвечает старой американской поговорке, Hollywood principle: «Не звони нам, мы тебе сами позвоним». Сами зависимости приходят к нам. Это в первую очередь паттерн, не библиотека. Поэтому в принципе такой паттерн распространен почти везде. Можно даже сказать, что все приложения так или иначе используют Dependency Injection.
![](https://habrastorage.org/r/w780q1/webt/wz/cj/8h/wzcj8hmirm_url9gghidooms19y.jpeg)
Посмотрим, как вообще Dependency Injection можно придумать самому, если мы начнем с нуля. Допустим, я решил разработать такой небольшой класс, в котором я буду создавать тикет через наш API. Создадим, например, инстанс класса TrackerApi. В нем есть метод createTicket, в который мы передадим мой имейл. Мы будем создавать тикет из-под моего аккаунта и с таким названием: «Подготовить доклад к Java Meetup».
![](https://habrastorage.org/r/w780q1/webt/sp/dn/md/spdnmdmw57qzfgfdfjtwqwijtiq.jpeg)
Посмотрим реализацию TrackerApi. Здесь мы, например, можем сделать так: создаем инстанс httpClient. Если говорить простым языком, то мы будем создавать объект, через который будем ходить в API. Через этот объект мы будем вызывать у него метод execute.
![](https://habrastorage.org/r/w780q1/webt/x4/2a/iy/x42aiy9l2baljluoanesiq04gay.jpeg)
Например, пользовательский какой-нибудь. Я написал внешний код от этих классов, и он будет его использовать примерно так. Я создаю новый TicketCreator и вызываю у него метод createTicket.
![](https://habrastorage.org/r/w780q1/webt/g8/al/li/g8allib-7c35r0iulwej8fjgmzu.jpeg)
Здесь есть проблема — мы каждый раз при создании тикета будем создавать заново и заново httpClient, хотя вообще говоря, в этом нет никакой нужды. httpClients бывают очень серьезными для создания.
![](https://habrastorage.org/r/w780q1/webt/eh/pc/o3/ehpco32zfcarfgwgabxumx6deuo.jpeg)
Давайте попробуем это вынести. Здесь можно увидеть самый первый пример Dependency Injection в нашем коде. Обратите внимание, что мы сделали. Мы вынесли нашу переменную в поле и заполняем его в конструкторе. Тот факт, что мы заполняем его в конструкторе, означает, что к нам приходят зависимости. Это первый Dependency Injection.
![](https://habrastorage.org/r/w780q1/webt/x0/qw/2r/x0qw2rjibtfionm8och8p4fgxsm.jpeg)
Мы переложили ответственность на пользователей кода, поэтому теперь мы должны создавать httpClient, передавая его, например, в TicketCreator.
![](https://habrastorage.org/r/w780q1/webt/x0/qw/2r/x0qw2rjibtfionm8och8p4fgxsm.jpeg)
Здесь это тоже не очень хорошо, потому что теперь мы, вызывая этот метод, опять будем каждый раз создавать httpClient.
![](https://habrastorage.org/r/w780q1/webt/tk/ib/zn/tkibznrpcl80hnoqg7qccgvl0em.jpeg)
Поэтому снова вынесем его в поле. И здесь, кстати, есть неочевидный пример Dependency Injection. Мы можем сказать, что мы всегда создаем из-под меня (или из-под кого-то) тикеты. Мы каждый отдельный объект TicketCreator будем создавать из-под разных пользователей.
Например, этот будет создавать из-под меня, когда мы его создадим. И строка, которую мы передаем в конструктор, — это тоже Dependency Injection.
![](https://habrastorage.org/r/w780q1/webt/jl/uq/nb/jluqnb4_2lburqy9jwo3u2hsrg8.jpeg)
Как мы будем делать теперь? Создаем новый инстанс TrackerTicketCreator и вызываем метод. Теперь мы можем даже создать какой-нибудь кастомный метод, который будет создавать нам тикет с кастомным текстом. Например, создадим тикет «Нанять нового стажера».
![](https://habrastorage.org/r/w780q1/webt/zr/un/hp/zrunhp6rgmmvg55w5djkz9z1clo.jpeg)
Теперь попробуем посмотреть, как бы выглядел наш код, если бы мы хотели читать комментарии в этом тикете точно так же, из-под меня. Это примерно такой же код. Мы бы вызывали метод getComments у этого тикета.
![](https://habrastorage.org/r/w780q1/webt/qs/hs/ek/qshsek9l2xmb9xskiryj-m6rtzm.jpeg)
Как бы он выглядел? Если мы берем и дублируем эту функциональность в ридере комментариев, мы дублируем создание httpClient. Нам такое не подходит. Мы хотим от этого избавиться.
![](https://habrastorage.org/r/w780q1/webt/tb/cq/ot/tbcqotqdzrcfxgzfzsba-e9us54.jpeg)
Хорошо. Теперь давайте пробросим все эти параметры как Dependency Injection, как параметры конструктора.
![](https://habrastorage.org/r/w780q1/webt/qu/po/9k/qupo9kqigu94hoyo5tpcssyoxzg.jpeg)
Какая здесь проблема? Мы всё прокинули, но в пользовательском коде мы теперь пишем «бойлерплейт». Это какой-то ненужный код, который обычно должен написать пользователь, чтобы сделать относительно небольшое действие с точки зрения логики. Здесь мы постоянно должны будем создавать httpClient, API для него и выбирать user email. Каждый пользователь TicketCreator должен будет делать это сам. Это не ок. Мы сейчас попробуем посмотреть, как это будет выглядеть в библиотеках, когда мы попытаемся этого избежать.
А сейчас немного отклонимся в сторону и посмотрим, что такое Inversion of Control, потому что многие связывают Dependency Injection ровно с ним.
![](https://habrastorage.org/r/w780q1/webt/h9/kj/xc/h9kjxcwtgvzwilwyikkiydksznm.jpeg)
Inversion of Control — принцип программирования, при котором используемые нами объекты создаются не нами. Мы вообще никак не влияем на их lifecycle. Обычно сущность, создающая эти объекты, называется IoC-контейнером. Многие из вас уже здесь слышали про Spring. Spring-документация говорит, что IoC так же известны, как Dependency Injection. Они считают, что это одно и то же.
![](https://habrastorage.org/r/w780q1/webt/fb/ue/oi/fbueoiomwpuhzlrkq3j9acynvcy.jpeg)
Какие есть основные принципы? Объекты создает не прикладной код, а какой-то IoC-контейнер. Мы как пользователи библиотеки ничего не делаем, к нам все само приходит. Конечно, IoC относительно. Сам IoC-контейнер создает эти объекты, и к нему это уже не применимо. Можно подумать о том, что IoC реализует не только DI-библиотеки. Известны библиотеки Java Servlets и Akka Actors, которые сейчас используются в Scala и в Java-коде.
![](https://habrastorage.org/r/w780q1/webt/mg/cj/qu/mgcjqu4qucgjz1w27guicfvgji8.jpeg)
Поговорим про библиотеки. Вообще говоря, библиотек для Java и Kotlin уже написано достаточно много. Я перечислю основные:
— Spring, большой фреймворк. Его основная часть — Dependency Injection или, как они говорят, Inversion of Control.
— Guice — библиотека, которая была написана примерно между вторым и третьим Spring, когда Spring переходил от XML к кодовому описанию. То есть когда Spring еще был не таким красивым.
— Dagger — то, чем обычно пользуются люди на Android.
Давайте попробуем переписать пример наш на Spring.
![](https://habrastorage.org/r/w780q1/webt/n-/tq/bl/n-tqblihbupgiumevmkyjad7wnc.jpeg)
У нас был наш TrackerApi. Я здесь не стал включать пользователя для краткости. Допустим, мы попытаемся в Dependency Injection сделать для httpClient. Для этого нам потребуется объявить его с аннотацией. Component, весь класс, и конкретно конструктор объявить с аннотацией Autowired. Что это означает для Spring?
![](https://habrastorage.org/r/w780q1/webt/qj/ef/i8/qjefi88q9vtuab9n16-rchaphzw.jpeg)
У нас в коде есть такая конфигурация, она обозначена аннотацией ComponentScan. Она означает, что мы попытаемся пройти по всему дереву наших классов в package, в котором он содержится. И дальше вглубь попытаемся найти все классы, которые размечены в аннотации Component.
![](https://habrastorage.org/r/w780q1/webt/n-/tq/bl/n-tqblihbupgiumevmkyjad7wnc.jpeg)
Эти компоненты попадут в IoC-контейнер. Для нас важно, что все попадет за нас. Мы только размечаем, что мы хотим объявить. Чтобы к нам что-то пришло, мы должны объявить это с помощью аннотации Autowired в конструкторе.
![](https://habrastorage.org/r/w780q1/webt/ym/hq/le/ymhqlevva5gqgnoieckrw6ymm2k.jpeg)
TicketCreator мы размечаем точно так же.
![](https://habrastorage.org/r/w780q1/webt/xk/fa/i_/xkfai_wibiwgnp1lpuzfi9fhbcs.jpeg)
И CommentReader тоже.
![](https://habrastorage.org/r/w780q1/webt/qj/ef/i8/qjefi88q9vtuab9n16-rchaphzw.jpeg)
Теперь посмотрим обратно на конфигурацию. Как мы уже говорили, Component Scan положит все в IoC-контейнер. Но здесь есть один момент, так называемый factory method. У нас есть метод httpClient, который мы создаем не в виде класса, потому что httpClient к нам приходит из библиотеки. У него нет никакого понимания, что такое Spring и т. д. Мы его создадим напрямую в конфигурации. Для этого мы пишем метод, который обычно билдит его один раз, и размечаем его аннотацией Bean.
![](https://habrastorage.org/r/w780q1/webt/br/ba/rf/brbarflmwiprwpmlknhdqpl5muc.jpeg)
Какие есть плюсы и минусы? Главный плюс — Spring очень распространен в мире. Следующим плюсом и одновременно минусом является автосканирование. Мы нигде не должны явно прописывать, что мы хотим добавить в IoC контейнер помимо аннотаций над самими классами. Достаточно аннотаций. И минус точно такой же: если мы, наоборот, хотим контроля над этим, то Spring нам этого не предоставляет. Разве что мы можем у себя в команде сказать: «Нет, мы так делать не будем. Мы должны явно что-то где-то прописать. Только так, в конфигурации, как мы сейчас делали с бинами.
Также из-за этого происходит медленный старт. Когда стартует приложение, Spring должен пройтись по всем этим классам и понять, что же положить в IoC-контейнер. Это замедляет его. Самым большим минусом Spring, как мне кажется, является дерево зависимостей. Оно проверяется не на этапе компиляций. Когда в определенный момент Spring запускается, ему надо понять, есть ли такая зависимость у меня внутри. Если потом окажется, что в дереве зависимостей его нет, то вы получите ошибку в runtime. А мы в Java не хотим ошибки в runtime. Мы хотим, чтобы код нам можно было скомпилировать. Это означает, что он работает.
![](https://habrastorage.org/r/w780q1/webt/nk/2l/6x/nk2l6x7k1fc6-undofdq53ppj8i.jpeg)
Давайте посмотрим на Guice. Это библиотека, которая, как я уже говорил, была сделана между вторым и третьим Spring. Той красоты, которую мы видели, не было. Были XML. Чтобы исправить эту проблему, и был написан Guice. А здесь видно, что, в отличие от конфигурации, мы пишем модуль. В нем мы объявляем явно, какие классы мы хотим положить в этот модуль: TrackerAPI, TrackerTicketCreator и все остальные бины. Аналогом Bean-аннотации здесь служит Provides, который создает httpClient в той же манере.
![](https://habrastorage.org/r/w780q1/webt/tf/wc/hn/tfwchnamip5ovyhno-7j_9twrae.jpeg)
Нам нужно объявить каждый из этих бинов. Мы назовем пример Singleton. Но конкретно Singleton скажет, что такой бин будет создан ровно один раз. Мы не будем его постоянно пересоздавать. А Inject, соответственно, является аналогом Autowired.
![](https://habrastorage.org/r/w780q1/webt/32/i9/-5/32i9-5iuc58qvzmpiahf_3odv6k.jpeg)
Небольшая табличка с тем, что к чему относится.
![](https://habrastorage.org/r/w780q1/webt/ny/5o/i4/ny5oi43in62gaprwj4ym5tvuxh8.jpeg)
Какие есть плюсы и минусы? Плюсы: он более простой, как мне кажется, и понятный, чем XML-вариант Spring. Более быстрый запуск. И отсюда минусы: он требует явного объявления используемых бинов. Мы должны были написать Bean. Но с другой стороны, это и плюс, как мы уже говорили. Это является зеркальным отражением того, что есть в Spring. Конечно, он менее распространен, чем Spring. Это его естественный минус. И есть точно такая же проблема — дерево зависимостей проверяется не на этапе компиляции.
Когда ребята начали использовать Guice для Android, они поняли, что им все равно не хватает скорости запуска. Поэтому они решили написать более простой и примитивный Dependency Injection-фреймворк, который позволит им сделать быстрый старт приложения, потому что для Android это очень важно.
![](https://habrastorage.org/r/w780q1/webt/5x/sp/l7/5xspl7f9z-2xoy22flag9sgrdzy.jpeg)
Здесь терминология та же. У Dagger есть точно такие же модули, как у Guice. Но они уже размечаются аннотациями, не как в случае наследования от класса. Поэтому принцип сохраняется.
Единственный минус — мы внутри модуля всегда должны явно указывать, каким образом создаются бины. В Guice мы могли отдать создание бинов внутрь самого бина. Нам не надо было говорить, какие зависимости куда нужно куда пробросить. А здесь нам нужно это явно сказать.
![](https://habrastorage.org/r/w780q1/webt/tu/q7/f8/tuq7f8jjaj-ouvsw1faz8e_8wgm.jpeg)
В Dagger из-за того, что не хочется делать слишком ручной ввод, есть понятие компонента. Компонент — это нечто, что связывает модули, когда мы хотим один бин из одного модуля объявить, чтобы его можно было взять в другом модуле. Это другая концепция. Бин из одного модуля может «заинжектить» бин из другого модуля с помощью компонента.
![](https://habrastorage.org/r/w780q1/webt/16/sn/41/16sn412k1exgnqbuil1mogbtpom.jpeg)
Здесь примерно такая же сводная табличка — что на что поменялось либо не поменялось в случае Inject или модулей.
![](https://habrastorage.org/r/w780q1/webt/pm/v1/ur/pmv1urcl1djm48azfby8ux6uqbs.jpeg)
Какие плюсы? Он еще более простой, чем Guice. Запуск происходит еще быстрее, чем у Guice. И он уже, наверное, не станет более быстрым, потому что в Dagger полностью отказались от reflection. Это именно та часть библиотеки в Java, которая отвечает за то, чтобы посмотреть на состояние объекта, на его класс и методы. То есть получить состояние в runtime. Поэтому он не использует reflection. Он не идет и не сканирует, какие у кого зависимости. Но из-за этого он и стартует очень быстро.
Как он это делает? С помощью кодогенерации.
![](https://habrastorage.org/r/w780q1/webt/tu/q7/f8/tuq7f8jjaj-ouvsw1faz8e_8wgm.jpeg)
Если мы посмотрим назад, то увидим компонент интерфейса. Мы не сделали никакую реализацию этого интерфейса, Dagger сам за нас ее делает. И можно будет дальше использовать интерфейс в приложении.
![](https://habrastorage.org/r/w780q1/webt/pm/v1/ur/pmv1urcl1djm48azfby8ux6uqbs.jpeg)
Естественно, он очень сильно распространен в мире Android за счет этой скорости. Дерево зависимостей проверяется сразу на компиляции, потому что нет ничего, что мы отложенно будем проверять в runtime.
Какие здесь минусы? У него меньше возможностей. Он более многословный, чем Guice и Spring.
![](https://habrastorage.org/r/w780q1/webt/cx/m8/ng/cxm8ngienchgjtm4yc1m7uo26bo.jpeg)
В рамках этих библиотек в Java возникла инициатива — так называемый JSR-330. JSR — это запрос на то, чтобы сделать изменение в спецификации языка либо дополнить его какими-то дополнительными библиотеками. Такой стандарт предложили на основе Guice, и аннотации Inject были внесены в эту библиотеку. Соответственно, Spring и Guice его поддерживают.
Какие можно сделать выводы? В Java очень много разных библиотек для DI. И надо понимать, для чего мы берем конкретную из них. Если мы берем Android, то тут уже выбора нет, используем Dagger. Если мы идем в мир бэкенда, то уже смотрим, что нам больше подходит. И для первого изучения Dependency Injection, как мне кажется, Guice подходит лучше, чем Spring. В нем нет ничего лишнего. Можно посмотреть, как это работает, пощупать.
Для дальнейшего изучения я предлагаю ознакомиться с документацией всех этих библиотек и составом JSR:
— Spring
— Guice
— Dagger 2
— JSR-330
Спасибо!
— Всем привет, меня зовут Кирилл. Я буду сегодня рассказывать про Dependency Injection.
Мы начнем с того, как у меня называется доклад. «В некотором царстве, не в «спринговом» государстве». Мы поговорим, конечно, про Spring, но также хочется посмотреть на все, что есть помимо него. О чем конкретно поговорим?
![](https://habrastorage.org/webt/cq/li/gv/cqligvmc0hnuu56z8mgqjjnoebo.jpeg)
Я сделаю небольшое отступление — расскажу, над чем я работаю, какой у меня проект, зачем мы используем Dependency Injection. Потом расскажу, что это вообще такое, сравню Inversion of Control и Dependency Injection, и расскажу о его реализации в трех самых известных библиотеках.
Я работаю в команде Яндекс.Трекера. Мы делаем продуктовый аналог Jira или Trello. [...] Мы решили сделать собственный продукт, который сначала был внутренним. Сейчас мы его продаем наружу. Каждый из вас может зайти, создать себе собственный Трекер и делать задачи — например, учебные или бизнесовые.
Посмотрим на интерфейс. В примерах я буду использовать некоторые термины из моей области. Мы попробуем создать тикет и в нем посмотрим на комментарии, которые будут мне оставлять другие коллеги.
Начну с того, что вообще такое Dependency Injection. Это такой паттерн программирования, который отвечает старой американской поговорке, Hollywood principle: «Не звони нам, мы тебе сами позвоним». Сами зависимости приходят к нам. Это в первую очередь паттерн, не библиотека. Поэтому в принципе такой паттерн распространен почти везде. Можно даже сказать, что все приложения так или иначе используют Dependency Injection.
![](https://habrastorage.org/webt/wz/cj/8h/wzcj8hmirm_url9gghidooms19y.jpeg)
Посмотрим, как вообще Dependency Injection можно придумать самому, если мы начнем с нуля. Допустим, я решил разработать такой небольшой класс, в котором я буду создавать тикет через наш API. Создадим, например, инстанс класса TrackerApi. В нем есть метод createTicket, в который мы передадим мой имейл. Мы будем создавать тикет из-под моего аккаунта и с таким названием: «Подготовить доклад к Java Meetup».
![](https://habrastorage.org/webt/sp/dn/md/spdnmdmw57qzfgfdfjtwqwijtiq.jpeg)
Посмотрим реализацию TrackerApi. Здесь мы, например, можем сделать так: создаем инстанс httpClient. Если говорить простым языком, то мы будем создавать объект, через который будем ходить в API. Через этот объект мы будем вызывать у него метод execute.
![](https://habrastorage.org/webt/x4/2a/iy/x42aiy9l2baljluoanesiq04gay.jpeg)
Например, пользовательский какой-нибудь. Я написал внешний код от этих классов, и он будет его использовать примерно так. Я создаю новый TicketCreator и вызываю у него метод createTicket.
![](https://habrastorage.org/webt/g8/al/li/g8allib-7c35r0iulwej8fjgmzu.jpeg)
Здесь есть проблема — мы каждый раз при создании тикета будем создавать заново и заново httpClient, хотя вообще говоря, в этом нет никакой нужды. httpClients бывают очень серьезными для создания.
![](https://habrastorage.org/webt/eh/pc/o3/ehpco32zfcarfgwgabxumx6deuo.jpeg)
Давайте попробуем это вынести. Здесь можно увидеть самый первый пример Dependency Injection в нашем коде. Обратите внимание, что мы сделали. Мы вынесли нашу переменную в поле и заполняем его в конструкторе. Тот факт, что мы заполняем его в конструкторе, означает, что к нам приходят зависимости. Это первый Dependency Injection.
![](https://habrastorage.org/webt/x0/qw/2r/x0qw2rjibtfionm8och8p4fgxsm.jpeg)
Мы переложили ответственность на пользователей кода, поэтому теперь мы должны создавать httpClient, передавая его, например, в TicketCreator.
![](https://habrastorage.org/webt/x0/qw/2r/x0qw2rjibtfionm8och8p4fgxsm.jpeg)
Здесь это тоже не очень хорошо, потому что теперь мы, вызывая этот метод, опять будем каждый раз создавать httpClient.
![](https://habrastorage.org/webt/tk/ib/zn/tkibznrpcl80hnoqg7qccgvl0em.jpeg)
Поэтому снова вынесем его в поле. И здесь, кстати, есть неочевидный пример Dependency Injection. Мы можем сказать, что мы всегда создаем из-под меня (или из-под кого-то) тикеты. Мы каждый отдельный объект TicketCreator будем создавать из-под разных пользователей.
Например, этот будет создавать из-под меня, когда мы его создадим. И строка, которую мы передаем в конструктор, — это тоже Dependency Injection.
![](https://habrastorage.org/webt/jl/uq/nb/jluqnb4_2lburqy9jwo3u2hsrg8.jpeg)
Как мы будем делать теперь? Создаем новый инстанс TrackerTicketCreator и вызываем метод. Теперь мы можем даже создать какой-нибудь кастомный метод, который будет создавать нам тикет с кастомным текстом. Например, создадим тикет «Нанять нового стажера».
![](https://habrastorage.org/webt/zr/un/hp/zrunhp6rgmmvg55w5djkz9z1clo.jpeg)
Теперь попробуем посмотреть, как бы выглядел наш код, если бы мы хотели читать комментарии в этом тикете точно так же, из-под меня. Это примерно такой же код. Мы бы вызывали метод getComments у этого тикета.
![](https://habrastorage.org/webt/qs/hs/ek/qshsek9l2xmb9xskiryj-m6rtzm.jpeg)
Как бы он выглядел? Если мы берем и дублируем эту функциональность в ридере комментариев, мы дублируем создание httpClient. Нам такое не подходит. Мы хотим от этого избавиться.
![](https://habrastorage.org/webt/tb/cq/ot/tbcqotqdzrcfxgzfzsba-e9us54.jpeg)
Хорошо. Теперь давайте пробросим все эти параметры как Dependency Injection, как параметры конструктора.
![](https://habrastorage.org/webt/qu/po/9k/qupo9kqigu94hoyo5tpcssyoxzg.jpeg)
Какая здесь проблема? Мы всё прокинули, но в пользовательском коде мы теперь пишем «бойлерплейт». Это какой-то ненужный код, который обычно должен написать пользователь, чтобы сделать относительно небольшое действие с точки зрения логики. Здесь мы постоянно должны будем создавать httpClient, API для него и выбирать user email. Каждый пользователь TicketCreator должен будет делать это сам. Это не ок. Мы сейчас попробуем посмотреть, как это будет выглядеть в библиотеках, когда мы попытаемся этого избежать.
А сейчас немного отклонимся в сторону и посмотрим, что такое Inversion of Control, потому что многие связывают Dependency Injection ровно с ним.
![](https://habrastorage.org/webt/h9/kj/xc/h9kjxcwtgvzwilwyikkiydksznm.jpeg)
Inversion of Control — принцип программирования, при котором используемые нами объекты создаются не нами. Мы вообще никак не влияем на их lifecycle. Обычно сущность, создающая эти объекты, называется IoC-контейнером. Многие из вас уже здесь слышали про Spring. Spring-документация говорит, что IoC так же известны, как Dependency Injection. Они считают, что это одно и то же.
![](https://habrastorage.org/webt/fb/ue/oi/fbueoiomwpuhzlrkq3j9acynvcy.jpeg)
Какие есть основные принципы? Объекты создает не прикладной код, а какой-то IoC-контейнер. Мы как пользователи библиотеки ничего не делаем, к нам все само приходит. Конечно, IoC относительно. Сам IoC-контейнер создает эти объекты, и к нему это уже не применимо. Можно подумать о том, что IoC реализует не только DI-библиотеки. Известны библиотеки Java Servlets и Akka Actors, которые сейчас используются в Scala и в Java-коде.
![](https://habrastorage.org/webt/mg/cj/qu/mgcjqu4qucgjz1w27guicfvgji8.jpeg)
Поговорим про библиотеки. Вообще говоря, библиотек для Java и Kotlin уже написано достаточно много. Я перечислю основные:
— Spring, большой фреймворк. Его основная часть — Dependency Injection или, как они говорят, Inversion of Control.
— Guice — библиотека, которая была написана примерно между вторым и третьим Spring, когда Spring переходил от XML к кодовому описанию. То есть когда Spring еще был не таким красивым.
— Dagger — то, чем обычно пользуются люди на Android.
Давайте попробуем переписать пример наш на Spring.
![](https://habrastorage.org/webt/n-/tq/bl/n-tqblihbupgiumevmkyjad7wnc.jpeg)
У нас был наш TrackerApi. Я здесь не стал включать пользователя для краткости. Допустим, мы попытаемся в Dependency Injection сделать для httpClient. Для этого нам потребуется объявить его с аннотацией. Component, весь класс, и конкретно конструктор объявить с аннотацией Autowired. Что это означает для Spring?
![](https://habrastorage.org/webt/qj/ef/i8/qjefi88q9vtuab9n16-rchaphzw.jpeg)
У нас в коде есть такая конфигурация, она обозначена аннотацией ComponentScan. Она означает, что мы попытаемся пройти по всему дереву наших классов в package, в котором он содержится. И дальше вглубь попытаемся найти все классы, которые размечены в аннотации Component.
![](https://habrastorage.org/webt/n-/tq/bl/n-tqblihbupgiumevmkyjad7wnc.jpeg)
Эти компоненты попадут в IoC-контейнер. Для нас важно, что все попадет за нас. Мы только размечаем, что мы хотим объявить. Чтобы к нам что-то пришло, мы должны объявить это с помощью аннотации Autowired в конструкторе.
![](https://habrastorage.org/webt/ym/hq/le/ymhqlevva5gqgnoieckrw6ymm2k.jpeg)
TicketCreator мы размечаем точно так же.
![](https://habrastorage.org/webt/xk/fa/i_/xkfai_wibiwgnp1lpuzfi9fhbcs.jpeg)
И CommentReader тоже.
![](https://habrastorage.org/webt/qj/ef/i8/qjefi88q9vtuab9n16-rchaphzw.jpeg)
Теперь посмотрим обратно на конфигурацию. Как мы уже говорили, Component Scan положит все в IoC-контейнер. Но здесь есть один момент, так называемый factory method. У нас есть метод httpClient, который мы создаем не в виде класса, потому что httpClient к нам приходит из библиотеки. У него нет никакого понимания, что такое Spring и т. д. Мы его создадим напрямую в конфигурации. Для этого мы пишем метод, который обычно билдит его один раз, и размечаем его аннотацией Bean.
![](https://habrastorage.org/webt/br/ba/rf/brbarflmwiprwpmlknhdqpl5muc.jpeg)
Какие есть плюсы и минусы? Главный плюс — Spring очень распространен в мире. Следующим плюсом и одновременно минусом является автосканирование. Мы нигде не должны явно прописывать, что мы хотим добавить в IoC контейнер помимо аннотаций над самими классами. Достаточно аннотаций. И минус точно такой же: если мы, наоборот, хотим контроля над этим, то Spring нам этого не предоставляет. Разве что мы можем у себя в команде сказать: «Нет, мы так делать не будем. Мы должны явно что-то где-то прописать. Только так, в конфигурации, как мы сейчас делали с бинами.
Также из-за этого происходит медленный старт. Когда стартует приложение, Spring должен пройтись по всем этим классам и понять, что же положить в IoC-контейнер. Это замедляет его. Самым большим минусом Spring, как мне кажется, является дерево зависимостей. Оно проверяется не на этапе компиляций. Когда в определенный момент Spring запускается, ему надо понять, есть ли такая зависимость у меня внутри. Если потом окажется, что в дереве зависимостей его нет, то вы получите ошибку в runtime. А мы в Java не хотим ошибки в runtime. Мы хотим, чтобы код нам можно было скомпилировать. Это означает, что он работает.
![](https://habrastorage.org/webt/nk/2l/6x/nk2l6x7k1fc6-undofdq53ppj8i.jpeg)
Давайте посмотрим на Guice. Это библиотека, которая, как я уже говорил, была сделана между вторым и третьим Spring. Той красоты, которую мы видели, не было. Были XML. Чтобы исправить эту проблему, и был написан Guice. А здесь видно, что, в отличие от конфигурации, мы пишем модуль. В нем мы объявляем явно, какие классы мы хотим положить в этот модуль: TrackerAPI, TrackerTicketCreator и все остальные бины. Аналогом Bean-аннотации здесь служит Provides, который создает httpClient в той же манере.
![](https://habrastorage.org/webt/tf/wc/hn/tfwchnamip5ovyhno-7j_9twrae.jpeg)
Нам нужно объявить каждый из этих бинов. Мы назовем пример Singleton. Но конкретно Singleton скажет, что такой бин будет создан ровно один раз. Мы не будем его постоянно пересоздавать. А Inject, соответственно, является аналогом Autowired.
![](https://habrastorage.org/webt/32/i9/-5/32i9-5iuc58qvzmpiahf_3odv6k.jpeg)
Небольшая табличка с тем, что к чему относится.
![](https://habrastorage.org/webt/ny/5o/i4/ny5oi43in62gaprwj4ym5tvuxh8.jpeg)
Какие есть плюсы и минусы? Плюсы: он более простой, как мне кажется, и понятный, чем XML-вариант Spring. Более быстрый запуск. И отсюда минусы: он требует явного объявления используемых бинов. Мы должны были написать Bean. Но с другой стороны, это и плюс, как мы уже говорили. Это является зеркальным отражением того, что есть в Spring. Конечно, он менее распространен, чем Spring. Это его естественный минус. И есть точно такая же проблема — дерево зависимостей проверяется не на этапе компиляции.
Когда ребята начали использовать Guice для Android, они поняли, что им все равно не хватает скорости запуска. Поэтому они решили написать более простой и примитивный Dependency Injection-фреймворк, который позволит им сделать быстрый старт приложения, потому что для Android это очень важно.
![](https://habrastorage.org/webt/5x/sp/l7/5xspl7f9z-2xoy22flag9sgrdzy.jpeg)
Здесь терминология та же. У Dagger есть точно такие же модули, как у Guice. Но они уже размечаются аннотациями, не как в случае наследования от класса. Поэтому принцип сохраняется.
Единственный минус — мы внутри модуля всегда должны явно указывать, каким образом создаются бины. В Guice мы могли отдать создание бинов внутрь самого бина. Нам не надо было говорить, какие зависимости куда нужно куда пробросить. А здесь нам нужно это явно сказать.
![](https://habrastorage.org/webt/tu/q7/f8/tuq7f8jjaj-ouvsw1faz8e_8wgm.jpeg)
В Dagger из-за того, что не хочется делать слишком ручной ввод, есть понятие компонента. Компонент — это нечто, что связывает модули, когда мы хотим один бин из одного модуля объявить, чтобы его можно было взять в другом модуле. Это другая концепция. Бин из одного модуля может «заинжектить» бин из другого модуля с помощью компонента.
![](https://habrastorage.org/webt/16/sn/41/16sn412k1exgnqbuil1mogbtpom.jpeg)
Здесь примерно такая же сводная табличка — что на что поменялось либо не поменялось в случае Inject или модулей.
![](https://habrastorage.org/webt/pm/v1/ur/pmv1urcl1djm48azfby8ux6uqbs.jpeg)
Какие плюсы? Он еще более простой, чем Guice. Запуск происходит еще быстрее, чем у Guice. И он уже, наверное, не станет более быстрым, потому что в Dagger полностью отказались от reflection. Это именно та часть библиотеки в Java, которая отвечает за то, чтобы посмотреть на состояние объекта, на его класс и методы. То есть получить состояние в runtime. Поэтому он не использует reflection. Он не идет и не сканирует, какие у кого зависимости. Но из-за этого он и стартует очень быстро.
Как он это делает? С помощью кодогенерации.
![](https://habrastorage.org/webt/tu/q7/f8/tuq7f8jjaj-ouvsw1faz8e_8wgm.jpeg)
Если мы посмотрим назад, то увидим компонент интерфейса. Мы не сделали никакую реализацию этого интерфейса, Dagger сам за нас ее делает. И можно будет дальше использовать интерфейс в приложении.
![](https://habrastorage.org/webt/pm/v1/ur/pmv1urcl1djm48azfby8ux6uqbs.jpeg)
Естественно, он очень сильно распространен в мире Android за счет этой скорости. Дерево зависимостей проверяется сразу на компиляции, потому что нет ничего, что мы отложенно будем проверять в runtime.
Какие здесь минусы? У него меньше возможностей. Он более многословный, чем Guice и Spring.
![](https://habrastorage.org/webt/cx/m8/ng/cxm8ngienchgjtm4yc1m7uo26bo.jpeg)
В рамках этих библиотек в Java возникла инициатива — так называемый JSR-330. JSR — это запрос на то, чтобы сделать изменение в спецификации языка либо дополнить его какими-то дополнительными библиотеками. Такой стандарт предложили на основе Guice, и аннотации Inject были внесены в эту библиотеку. Соответственно, Spring и Guice его поддерживают.
Какие можно сделать выводы? В Java очень много разных библиотек для DI. И надо понимать, для чего мы берем конкретную из них. Если мы берем Android, то тут уже выбора нет, используем Dagger. Если мы идем в мир бэкенда, то уже смотрим, что нам больше подходит. И для первого изучения Dependency Injection, как мне кажется, Guice подходит лучше, чем Spring. В нем нет ничего лишнего. Можно посмотреть, как это работает, пощупать.
Для дальнейшего изучения я предлагаю ознакомиться с документацией всех этих библиотек и составом JSR:
— Spring
— Guice
— Dagger 2
— JSR-330
Спасибо!