Что такое inversion of control

Обновлено: 05.07.2024

Что такое Инверсия управления и Внедрение зависимостей (IoС & DI)

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

Инверсия управления (Inversion of Control, IoC) это определенный набор рекомендаций, позволяющих проектировать и реализовывать приложения используя слабое связывание отдельных компонентов. То есть, для того чтобы следовать принципам Инверсии управления нам необходимо:

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

Одним из видов конкретной реализации данных рекомендаций является механизм Внедрения зависимостей (Dependency Injection, DI). Он определяет две основные рекомендации:

  • модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций;
  • абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

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

Предположим, что мы решили написать свою собственную программу, выполняющую крипто вычисления, другим словом майнер. Любая криптовалюта основана на какой-либо хэш-функции (алгоритме). Предположим, что наша программа будет выполнять вычисления на алгоритме SHA256, для майнинга биткоина. Тогда мы получим следующую связь между классами:

No DI

No DI

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

DI

DI

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

Существует несколько конкретных паттернов Внедрения зависимостей:

  • Через конструктор (Constructor injection);
  • Через свойство класса (Setter injection);
  • Через аргумент метода (Method injection).

Давайте рассмотрим примеры реализации данных паттернов.

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

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

Давайте рассмотрим простой пример. Представьте себе, что я пишу программу, которая получает некоторую информацию от пользователя с помощью командной строки. Я мог бы сделать это как-то так:

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

Однако если бы я использовал оконную систему для чего-то похожего, я написал бы что-то, что работает с окном:

Теперь между этими двумя программами большая разница в потоке управления — в частности, в управлении временем, когда вызываются методы process_name и process_quest. В примере с коммандной строкой я контролирую, когда эти методы вызываются, но в примере с оконным приложением нет. Вместо этого я передаю контроль оконной системе (команда Tk.mainloop). Далее она решает, когда вызвать мои методы, основываясь на связях, которые я настроил при создании формы. Управление инвертировано — управляют мной, а не я управляю фреймворком. Это явление и называется инверсией управления (также известно как Принцип Голливуда — «Не звони нам, мы сами позвоним тебе» — Hollywood Principle — «Don't call us, we'll call you»).

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

Ральф Джонсон и Брайан Фут.

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

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

Существуют различные способы подключить ваш код для его дальнейшего вызова. В предыдущем примере на ruby, мы вызываем метод bind текстового поля, который принимает имя события и замыкание в качестве аргументов. Всякий раз, когда текстовое поле узнает о событии, оно вызывает наш код из замыкания. Использование таких замыканий очень удобно, но многие языки не поддерживают их.

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

EJB-компоненты являются хорошим примером такого стиля инверсии управления. При разработке сессионного компонента(session bean), вы можете реализовать различные методы, которые вызываются EJB-контейнером в различных точках/состояниях жизненного цикла. Например, у интерфейса SessionBean есть методы ejbRemove, ejbPassivate (сохранен во вторичное хранилище) и ejbActivate (восстановлен из пассивного состояния). Вы не можете управлять вызовом этих методов, только тем, что они делают. Контейнер вызывает нас, а не мы вызываем его.

Примечание перевода, пример:

Это сложные случаи инверсии управления, но вы столкнетесь с этим в гораздо более простых ситуациях. Шаблонный метод является хорошим примером: супер-класс определяет поток управления, субклассы наследуются от него переопределяя методы или реализуя абстрактные методы. Например, в JUnit, код фреймворка вызывает методы setUp и tearDown для вас, чтобы создавать и очищать ваш тест. Происходит вызов, ваш код реагирует — это снова инверсия управления.

Примечание перевода, пример:

В наши дни, в связи с ростом количества IoC-контейнеров, существует некоторая путаница со смыслом инверсии управления. Некоторые люди путают общий принцип с конкретными стилями инверсии управления (такими как внедрение зависимостей), которые эти контейнеры используют. Все это немного запутанно (и иронично), так как IoC-контейнеры, как правило, рассматриваются в качестве конкурента EJB, но EJB использует инверсию управления.

Этимология: Насколько я могу судить, термин инверсии управления впервые появился на свет в работе Джонсона и Фута Designing Reusable Classes, опубликованной в журнале Object-Oriented Programming в 1988 году. Работа является одной из тех, что в возрасте хороши — ее можно прочитать даже спустя пятнадцать лет. Они считают, что они взяли этот термин откуда-то еще, но не могут вспомнить откуда. Затем термин втерся в объектно-ориентированное сообщество и вновь появился в книге Gang of Four. Более красивый синоним «Принцип Голливуда», кажется, берет начало в работе Ричарда Свита в Mesa в 1983 году. В списке целей разработки он пишет: Не звони нам, мы сами позвоним тебе (Закон Голливуда): инструмент должен организовать Тахо, чтобы предупредить его, когда пользователь захочет передать какое-то событие инструменту, вместо того, чтобы принимать модель «запросить у пользователя команду и выполнить ее». Джон Влиссидес пишет колонку о C++, которая несет в себе хорошее объяснение концепции под названием «Принцип Голливуда». (Спасибо Брайану Футу и Ральфу Джонсону за помощь с этимологией).

Реализация без инверсия управления

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

Теперь нам только и осталось создать экземпляр нашего майнера и запустить процесс майнинга.

В результате мы получим следующее:

Result no DI

Result no DI

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

Инверсия управления (Inversion of Control) – что это

Ключевая особенность приложения, написанного на Spring, состоит в том что большую часть объектов создаем не мы, а Spring. Мы лишь конфигурируем классы (с помощью аннотаций либо в конфигурационном XML), чтобы «объяснить» фреймворку Spring, какие именно объекты он должен создать за нас, и полями каких объектов их сделать. Spring управляет созданием объектов и потому его контейнер называется IoC-контейнер. IoC расшифровывается как Inversion of Control. А объекты, которые создаются контейнером и находятся под его управлением, называются бинами.

Иллюстрировать это можно так:


В общем на вход контейнер Spring принимает:

  • Наши обычные классы (которые впоследствии будут бинами).
  • Конфигурацию (неважно как именно ее задавать – либо в специальном файле XML, либо с помощью специальных аннотаций).

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

IoC, DI, IoC-контейнер — Просто о простом

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

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

Теория

Для меня взаимосвязь между IoC и DI такая же как между Agile и Scrum, т.е.
Inversion of Control (инверсия управления) — это некий абстрактный принцип, набор рекомендаций для написания слабо связанного кода. Суть которого в том, что каждый компонент системы должен быть как можно более изолированным от других, не полагаясь в своей работе на детали конкретной реализации других компонентов.
Dependency Injection (внедрение зависимостей) — это одна из реализаций этого принципа (помимо этого есть еще Factory Method, Service Locator).
IoC-контейнер — это какая-то библиотека, фреймворк, программа если хотите, которая позволит вам упростить и автоматизировать написание кода с использованием данного подхода на столько, на сколько это возможно. Их довольно много, пользуйтесь тем, чем вам будет удобно, я продемонстрирую все на примере Ninject.

Практика

Схема взаимодействия клиента с сервисом

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

То как технически это будет сделано и определяет каждая из реализаций подхода IoC.
Мы будем использовать DI, на простом примере:
Скажем есть некий класс, который создает расписание, а другой класс его отображает (их как правило много, скажем один для десктоп-приложения, другой для веба и т.д.).
Если бы мы ничего не знали о IoC, DI мы бы написали что-то вроде этого:

Вроде бы все хорошо, код решает поставленную задачу, но что если мы захотим в последствии изменить реализацию менеджера расписаний и/или же иметь несколько таких менеджеров и динамически их заменять. Тогда в последствии нам придется менять и что-то в ScheduleViewer, а значит и снова его тестировать.
К счастью, разработчики, ленивые люди в хорошем смысле этого слова, и не любят делать одно и тоже дважды.

Мы, например, воспользуемся внедрением зависимостей (DI) для того, чтобы разорвать этот клубок стальных ниток — сделаем связь между этими классами более слабой, добавив прослойку в виде интерфейса IScheduleManager. И будем разрешать ее одним из способов техники DI, а именно Constructor Injection (помимо этого есть Setter Injection и Method Injection — если в двух словах, то везде используется интерфейс вместо конкретного класса, например в типе свойства или в типе аргумента метода):

И далее там где мы хотим воспользоваться нашим классом для отображения расписания мы пишем:

Вот уже почти идеально, но что если у нас много различных ScheduleViewer, разбросанных по проекту, которые использует всегда именно ScheduleManager (придется его руками каждый раз создавать) и/или мы хотим как-либо настроить поведение, так что бы в одной ситуации везде использовать ScheduleManager, а в другой скажем AnotherScheduleManager и т.д.
Решить эту проблему как раз и призваны IoC-контейнеры.

IoC-контейнеры

Они помогают уменьшить количество рутины, позволяя задать соответствие между интерфейсом и его конкретной реализацией, чтобы потом везде этим пользоваться.
Как я уже говорил выше, мы будем рассматривать это на примере Ninject —
1. Сначала мы создаем конфигурацию контейнера:

Теперь везде где требуется IScheduleManager будет подставляться ScheduleManager.
2. Создаем сам контейнер, указывая его конфигуратор:

Контейнер сам создаст экземпляр класса ScheduleManager, вызовет конструктор ScheduleViewer и подставит в него свежесозданный экземпляр ScheduleManager.

Внедрение зависимостей (DI)

Для начала добавим интерфейс для алгоритмов поиска хешей.

Создадим новый класс алгоритма Ethash и изменим существующий SHA256.

Внедрение зависимостей через конструктор

Изменим класс майнер следующим образом.

Как видите, теперь майнер не зависит от конкретного алгоритма, а принимает только интерфейс как аргумент конструктора.

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


Внедрение зависимостей через свойство


Внедрение зависимостей через аргумент метода


IoC-контейнер

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

Рекомендую также изучить статью SOLID в объектно-ориентированном программировании. Инверсия управления входит в набор этих принципов, поэтому целесообразно изучить остальные. А еще подписывайтесь на группу ВКонтакте, Telegram и YouTube-канал. Там еще больше полезного и интересного для программистов.

Работа с IoC-контейнером в Spring

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

Конфигурация бинов с помощью аннотаций

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

Во-первых, создадим класс конфигурации и аннотируем его с помощью @Configuration:

Во-вторых, допустим, у нас есть класс Animal, который мы хотим делать бином:

Чтобы сделать Animal бином, создадим в классе конфигурации метод,который создает и возвращает Animal, и аннотируем этот метод с помощью @Bean:

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

Возможно вы спросите, в чем смысл этого, ведь все равно мы создаем экземпляр Animal отдельным методом. Где же польза контейнера?

Да, мы прописываем метод, создающий Animal. Но мы не вызываем этот метод. Мы просто создаем контейнер. А метод вызывается контейнером во время его инициализации. И учитывая то, что бины обычно конфигурируются однотипно, выгода есть. К тому же они обычно создаются контейнером в единственном экземпляре (хотя это зависит от конфигурации) и внедряются в поля других бинов согласно конфигурации автоматически.

Причем вместо создания аннотированного @Bean метода, можно аннотировать класс Animal изнутри аннотацией @Component – а это и вовсе одна строчка.

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

Разница между аннотациями @Bean и @Component в том, что @Bean более гибкая аннотация, ею мы аннотируем метод, а не класс:

  • С помощью @Bean можно конфигурировать бины для тех классов, код которых вы не можете редактировать, например, классы из чужих библиотек.
  • С помощью @Bean можно также конфигурировать классы, создаваемые фабричными методами.

Давайте сконфигурируем второй бин Man с помощью аннотации @Component:

Только учтите, что если мы аннотируем классы с помощью @Component, то в файл конфигурации надо добавить аннотацию @ComponentScan для автоматического поиска этих аннотированных классов. В ней надо указать имя пакета или нескольких пакетов, в который лежат эти классы. В этом пакете контейнер будет пытаться их найти автоматически.

Давайте добавим в файл конфигурации одну строчку с аннотацией @ComponentScan и именем пакета для поиска бинов:

Проверим, что оба бина действительно создаются:

Мы рассмотрели, что такое IoC-контейнер, как его создать и как из него получить бины. Также мы узнали, что можно сконфигурировать бины с помощью аннотаций @Bean и @Component.

Что такое Инверсия управления и Внедрение зависимостей (IoС & DI)

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

Инверсия управления (Inversion of Control, IoC) это определенный набор рекомендаций, позволяющих проектировать и реализовывать приложения используя слабое связывание отдельных компонентов. То есть, для того чтобы следовать принципам Инверсии управления нам необходимо:

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

Одним из видов конкретной реализации данных рекомендаций является механизм Внедрения зависимостей (Dependency Injection, DI). Он определяет две основные рекомендации:

  • модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций;
  • абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

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

Предположим, что мы решили написать свою собственную программу, выполняющую крипто вычисления, другим словом майнер. Любая криптовалюта основана на какой-либо хэш-функции (алгоритме). Предположим, что наша программа будет выполнять вычисления на алгоритме SHA256, для майнинга биткоина. Тогда мы получим следующую связь между классами:

No DI

No DI

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

DI

DI

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

Существует несколько конкретных паттернов Внедрения зависимостей:

  • Через конструктор (Constructor injection);
  • Через свойство класса (Setter injection);
  • Через аргумент метода (Method injection).

Давайте рассмотрим примеры реализации данных паттернов.

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

Класс ApplicationContext для работы с IoC Контейнером

Конфигурация Maven

Чтобы иметь возможность работать с контейнером, добавьте в pom.xml зависимость:

Последнюю версию зависимости можно взять тут.

Класс ApplicationContext

Для работы с контейнером существует не один класс. Но удобнее всего работать с классом ApplicationContext. Чтобы инициализировать контейнер и создать в нем бины, нужно создать экземпляр класса ApplicationContext.

Как уже сказано, контейнеру для создания бинов. требуется конгфигурация, так что конструктор контейнера принимает аргумент. Существуют два подкласса ApplicationContext: ClassPathXmlApplicationContext берет конфигурацию из XML-файла, а AnnotationConfigApplicationContext – из аннотаций:

Так что контейнер можно создать, используя именно тот вид конфигурации, которая используется в вашем приложении. Обычно это аннотации, XML немного устарел.

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

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

Мы получили бин типа Animal из контейнера в предположении, что он сконфигурирован в нашем приложении. Давайте теперь его на самом деле сконфигурируем.

Что такое внедрение зависимости (Dependency Injection)

Ведь помимо создания объектов, Spring-контейнер внедряет эти объекты в другие объекты, то есть делает их полями других объектов. Иногда это выглядит магически – например, контейнер способен внедрить зависимость в поле с модификатором private, для которого нет сеттера. Как же код Spring может это сделать? Дело в том, что под капотом он использует рефлексию, так что это реально. Но эти детали для нас как разработчиков не важны, главное знать, как объяснить фреймворку, какие объекты вы хотите отдать под его управление, и в какие поля других объектов вы хотите их внедрить.

Как создать свой IoC и Dependency Injection из трех классов на стандартной Java и рефлексии читайте тут.

Читайте также: