Inversion of Control и Dependency Injection
В основе Spring лежит реализация двух основных паттернов - Inversion of Control и Dependency Injection (о том, что такое паттерны проектирования ПО, смотрите в главе Что учить из классической Computer Science?). Понять их может быть довольно сложно, но попробуем разобраться.
Inversion of Control
Обычно когда вам надо использовать какой-то сторонний код в своей программе, вы:
Подключаете библиотку с кодом
В своем коде вызываете какой-то метод из библиотеки
Ждете пока он завершится, после чего продолжаете выполнение своего кода
Выглядит это как-то так:
public static void main(String[] args) {
// Выполнение программы началось
// Сделали тут какую-то работу
SomeLibrary.someMethod(); // позвали код библиотеки
// Продолжили работу в своем коде
}
Этот очевидный и тривиальный вариант называется прямым потоком выполнения программы.
Но существует и другой вариант. Если библиотека, которую мы запрашиваем, содержит внутри какой-то сложный процесс, нам лучше отдать контроль за выполнением программы самой библиотеке. И лишь с использованием callback вызовов выдавать ей какие-либо данные.
Такой подход называется инверсией управления или inversion of control, IoC. Теперь получается, что не наш код рулит библиотекой, а библиотека рулит нашим кодом.
class OurClass {
public static String getData() {
// Когда библиотеке понадобятся от нас какие-то данные - она сама
// нас спросит, вызвав нужные методы в нужном ей порядке
// Нам нужно лишь определить эти методы, остальное библиотека берет на себя
}
public static void main(String[] args) {
// Позвали код библиотеки, передав ей те методы нашего кода, которые
// она будет вызывать когда захочет
// Вся дальнейшая работа идет внутри метода run(), он завершится только когда
// завершится работа программы
SomeLibrary.run(OurClass::getData);
}
}
Типичным примером такого подходя является любой GUI фреймворк. Например, вот так выглядит код на стандартном Java GUI фреймворке Swing (код из https://stackoverflow.com/questions/18978337/how-to-create-an-hello-world-in-java-swing-what-is-wrong-in-my-code). Это приложение рисует окошко с надписью HelloWorld:
public class HelloWorldSwing {
private static void createAndShowGUI() {
JFrame frame = new JFrame("HelloWorldSwing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JLabel label = new JLabel("Hello World");
frame.getContentPane().add(label);
frame.pack();
frame.setVisible(true);
}
// Точка входа в нашу программу
public static void main(String[] args) {
// Отдаем контроль фреймворку, передавая ему макет нашего интерфейса
javax.swing.SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGUI();
}
});
}
}
Мы создаем структуру нашего окна, кнопок и прочего интерфейса, затем вызываем метод фреймворка Swing и дальше работает уже он. При необходимости он вызывает наши callback методы (например, обработчики нажатий на кнопки пользователем).
Веб-серверы - еще один типичный пример IoC. Вы запускаете главный цикл веб-сервера и передаете ему управление, а он уже берет на себя все технические заботы - открывает соединения, принимает запросы по нужному протоколу, конвертирует параметры, и только когда возникает необходимость (сетевой запрос пришел и успешно декодирован) вызывает ваш код (обработчик этого запроса).
IoC паттерн хорошо подходит, если:
Требуется реализовать некий стандартный, сложный, долго работающий процесс. Тот же GUI фреймворк содержит в себе много работы по рендерингу, обработке событий, пользовательского ввода, которая под капотом в цикле идет каждую секунду пока приложение открыто. Мы не хотим реализовывать этот цикл самостоятельно, поэтому просто запускаем готовый цикл из фреймворка, отдаем ему управление и дальше делаем то, что он скажет.
Хочется разбить программу на отдельные слабо связанные блоки. В итоге компоненты нашей программы не вызывает друг друга, все взаимодействие идет через фреймворк. Тот запрашивает у нас данные и действия, мы их делаем и возвращаем управление ему. Каждый кусочек кода в таком случае получается изолированным, его проще тестировать и отлаживать.
Есть у IoC и недостатки. Код оказывается разбросан по куче мест, callback'ов и методов. Без детального понимания работы конкретного IoC фреймворка становится невозможно понять, что он делает и в каком порядке все это работает.
Spring является IoC фреймворком. Вы запускаете его, передаете ему конфигурацию (где искать классы, настройки, какие возможности фреймворка использовать) и дальше он сам что-то делает, время от времени по необходимости дергая ваш код.
Dependency Injection
В любом крупном проекте рано или поздно встает вопрос управления зависимостями в коде. Плодится число классов, одним классам для работы нужды экземпляры других классов, конструкторы становятся сложными и становится непонятно, где брать для них данные.
Например, у нас в проекте есть класс для работы с базой данных. Ему для работы нужен адрес, логин и пароль от БД, и он предоставляет некие методы для сохранения и загрузки данных:
class WishlistDao {
public WishlistDao(String url, String login, String pass) {...}
public List<WishlistEntry> getAll() {...}
public void saveEntry(WishlistEntry e) {...}
}
Дальше мы хотим создать класс, который умеет генерировать красивую HTML страничку со списком наших записей. Ему нужен доступ к БД, поэтому ему нужен доступ к WishlistDao
class WishlistPageGenerator {
WishlistDao dao = А где нам его взять ???
public String generateHtml() {...}
}
Мы можем создать этот объект прямо внутри нашего класса, но тогда нам придется как-то передавать в него параметры подключения.
А что делать, если у нас появится несколько классов, которым нужен доступ к БД? Например, у нас будет отдельная страница со статистикой. Или мы захотим время от времени очищать БД от старых записей. Или еще какие-либо манипуляции проделывать.
Заводить в каждом таком классе свою копию объекта подключения? Неочевидно, лишний расход памяти и не всегда возможно, так как внутри объекта могут использоваться неразделяемые ресурсы. Использовать паттерн Синглтон, создав один глобальный инстанс (подробнее про этот паттерн можно узнать тут https://refactoring.guru/ru/design-patterns/singleton)? Простейший Синглтон выглядит вот так и гарантирует, что у нас всегда будет единственный экземпляр этого объекта, доступный отовсюду как WishListDao.getInsance()
.
class WishListDao {
private static WishListDao instance;
public static void init(String url, String user, String pass) {
instance = new WishListDao(url, user, pass);
}
public static WishListDao getInstance() {return instance;}
private WishListDao(String url, String user, String pass) {...}
}
Для инициализации этого объекта нам один раз на старте приложения надо будет позвать WishListDao.init(url, user, pass)
и дальше все компоненты нашей программы смогут к нему обращаться.
Этот подход выглядит удобным, пока у нас всего пара-тройка классов. Но в реальности у нас могут быть десятки классов со сложными зависимостями между ними. Класс А имеет поля типов B и C, у класса B поля C и D, а у класса C - еще пяток разных. В каком порядке и как их создавать - это все быстро становится нетривиальной задачей, в которой очень легко запутаться.
Для решения этой проблемы и был придуман паттерн Dependency Injection. По сути он является разновидностью рассмотренного выше IoC.
В простом случае мы сами в своем коде управляем жизненным циклом объектов. Создаем те, что нам нужны, затем создаем те, которые от них зависят и так далее.
При использовании DI фреймворка мы говорим ему: "вот тебе куча классов, посмотри сам, какие у них там зависимости, сам выбери порядок, в котором их создавать и сам создай в правильном порядке, правильно проставив все ссылки друг на друга".
Конкретно в случае со Spring это работает так:
Создается Application Context. Можно рассматривать это как хранилище для всех объектов, которыми управляет Spring, этакая Map<String, Object>
Spring сканирует все классы по указанным путям (по умолчанию во всем проекте) в поисках тех классов, которыми он будет управлять.
Spring строит граф связей между классами и выясняет, какие из них от кого зависят, и какие надо создавать в первую очередь.
Если на этом этапе обнаруживаются циклы, то есть например класс A имеет поле типа B, а класс B - поле типа A - вылетает ошибка, так как непонятно, какой класс создавать первым, не имя при этом объекта второго класса. Однако есть способы эту ошибку обойти.
Создав все экземпляры всех классов, Spring пробегается по всем полям, помеченным специальной аннотацией @Autowired, и для каждого поля пытается найти в Application Context соответствующий ему объект. Если находит - проставляет значение поля, если не находит - кидает ошибку.
Например, у нас есть вот такая структура классов:
@Component
class A {
@Autowired
public B b;
}
@Component
class B {
@Autowired
public C c;
}
@Component
class C {}
Spring по аннотации @Component определит, что этот класс - это бин, которым он должен управлять. Построит граф зависимостей между классами, в данном случае тривиальный A-> B -> C, создаст инстанс класса С, затем класса B, затем запишет в поле B.c созданный им ранее объект С, затем то же сделает с А и А.b.
В итоге мы определяем только сами классы, их жизненным циклом управляет Spring. Нам не надо заботиться о том, как их создать и где взять значения параметров. В сложных приложениях с сотнями классов и разветвленными графами зависимостей между ними это огромная помощь и подспорье.
Last updated