Что такое inversion of control
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.
Инверсия управления и Внедрение зависимостей (IoС & DI)
Давайте рассмотрим что такое инверсия управления в языке программирования C#, для чего она нужна и какие проблемы он решает. Где можно применять данный шаблон, а где это будет излишним. Также давайте обратим внимание на её отличия от внедрение зависимостей, а также чем похожи IoС & DI.
Что такое Инверсия управления и Внедрение зависимостей (IoС & DI)
Паттерн (шаблон) проектирования — это продуманный способ построения исходного кода программы для решения часто возникающих в повседневном программировании проблем проектирования. Иными словами, это уже придуманное решения, для типичной задачи. При этом паттерн не готовое решение, а просто алгоритм действий, который должен привести к желаемому результату. Но сегодня мы рассмотрим не конкретный паттерн проектирования, а принцип построения объектно-ориентированных приложений Инверсия управления (IoC).
Инверсия управления (Inversion of Control, IoC) это определенный набор рекомендаций, позволяющих проектировать и реализовывать приложения используя слабое связывание отдельных компонентов. То есть, для того чтобы следовать принципам Инверсии управления нам необходимо:
Одним из видов конкретной реализации данных рекомендаций является механизм Внедрения зависимостей (Dependency Injection, DI). Он определяет две основные рекомендации:
То есть, если у нас будут существовать два связанных класса, то нам необходимо реализовывать связь между ними не напрямую, а через интерфейс. Это позволит нам при необходимости динамически менять реализацию зависимых классов.
Предположим, что мы решили написать свою собственную программу, выполняющую крипто вычисления, другим словом майнер. Любая криптовалюта основана на какой-либо хэш-функции (алгоритме). Предположим, что наша программа будет выполнять вычисления на алгоритме SHA256, для майнинга биткоина. Тогда мы получим следующую связь между классами:
Проблема состоит в том, что в настоящее время алгоритмов, на которых основаны крипто валюты достаточно много и их число постоянно увеличивается. Если мы захотим добавить новые алгоритмы для майнинга других крипто валют, нам придется вносить большое количество изменений в сам класс майнера. Чтобы этого избежать, необходимо создать промежуточный интерфейс, от которого будет зависеть майнер и который должны будут реализовывать различные алгоритмы.
Так мы сможем не только разделить ответственность за выполнение конкретных задач между классом майнером и алгоритмами, но и сделаем задел на дальнейшее увеличение количества поддерживаемых алгоритмов.
Существует несколько конкретных паттернов Внедрения зависимостей:
Давайте рассмотрим примеры реализации данных паттернов.
Для начала рассмотрим как можно реализовать данную задачу без Внедрения зависимостей. Мы создадим класс майнер, который будет запускать отдельный поток вычисления хеш-функции.
Реализация без инверсия управления
Так же мы создадим класс SHA256, который как раз и будет отвечать за нахождения хеша.
Теперь нам только и осталось создать экземпляр нашего майнера и запустить процесс майнинга.
В результате мы получим следующее:
Но как и упоминалось ранее, если мы захотим добавить новый алгоритм нам потребуется внести достаточно много изменений в код нашего класса майнера. Давайте внесем изменения в наши классы так, чтобы сделать их более гибкими и универсальными.
Внедрение зависимостей (DI)
Для начала добавим интерфейс для алгоритмов поиска хешей.
Создадим новый класс алгоритма Ethash и изменим существующий SHA256.
Внедрение зависимостей через конструктор
Изменим класс майнер следующим образом.
Как видите, теперь майнер не зависит от конкретного алгоритма, а принимает только интерфейс как аргумент конструктора.
Теперь нам немного нужно изменить вызов майнера в консольном приложении.
Внедрение зависимостей через свойство
Внедрение зависимостей через аргумент метода
Исходный код также можно посмотреть в репозитории https://github.com/shwanoff/DependencyInjection.
IoC-контейнер
Также хотелось бы упомянуть о IoC-контейнерах. IoC-контейнер (IoC-container) — это своеобразный фреймворк, позволяющий реализовывать Внедрение зависимостей более лаконичным способом, избавляя от рутины. Существует много различных IoC-контейнеров. Вот некоторые наиболее популярные из них:
В заключение хочется сказать, что IoC, DI — очень полезный механизм, но и как любой другой инструмент его нужно использовать только там, где он действительно необходим и принесет больше пользы, чем вреда. Ведь его реальное внедрение в небольшое консольное приложение, в котором вряд ли что-то будет меняться только усложнит архитектуру и понимание кода, а также увеличит вероятность совершения ошибки. В то же время если это серьезный крупный проект, где пожелания заказчика часто изменчивы и противоречивы, данный механизм может серьезно упростить жизнь разработчикам.
Рекомендую также изучить статью SOLID в объектно-ориентированном программировании. Инверсия управления входит в набор этих принципов, поэтому целесообразно изучить остальные. А еще подписывайтесь на группу ВКонтакте, Telegram и YouTube-канал. Там еще больше полезного и интересного для программистов.
Похожее
Похожие записи
Lazy command C# | Паттерн Ленивая команда C#
Чем инверсия управления (Inversion of Control, IoC) отличается от инверсии зависимостей (Dependency Inversion Principle, DIP)?
В литературе встречаются два разных понятия Инверсия управления и Принцип инверсии зависимостей, которые сформулированы одинакаво:
Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.
Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
В чем состоит разница?
1 ответ 1
Фреймворки предоставляют точки подключения, в которых вы можете писать свой код. Но при этом общим выполнением программы по-прежнему управляет фреймворк, время от времени вызывая ваш код. Инверсия заключается в том, что в отличии от традиционного подхода не вы вызываете библиотечный код, а библиотечный код вызывает вас. Примерами могут являться ASP.NET и WPF с их событиями.
При этом принцип инверсии управления может использоваться и в более мелком масштабе. Например, есть слой логики (BLL) и доступа к данным (DAL). Общеизвестно, что DAL не может обращаться к BLL. Но иногда возникает потребность на уровне DAL выполнить некий код, который должен принадлежать к BLL. В таком случае в DAL заводится некоторый интерфейс, этот интерфейс реализуется на уровне BLL, и экземпляр конкретной реализации передается из BLL в DAL. Такой пример, в частности, согласуется с DIP.
Dependency Inversion Principle дает рекомендации о том, какими должны быть зависимости. Это как раз то, что вы процитировали:
Вы также могли слышать о Dependency Injection. Внедрение зависимостей является связующим звеном между IoC и DIP. Это один из способов реализации инверсии управления.
Лучше почитайте английскую Вики про IoC и DIP, там намного лучше все объяснено. Думаю, даже с помощью Гугл транслейта это будет полезнее, чем статьи на русской Вики.
В качестве русскоязычного источника могу порекомендовать статью DI vs. DIP vs. IoC от Сергея Теплякова. Возможно он даже зайдет в этот вопрос и объяснит все как следует :).
Инверсия и внедрение зависимостей
В своей работе в Distillery я провожу много собеседований, и 90% того, что я слышу, когда задаю вопрос о внедрении зависимостей, откровенно говоря, полная чушь. Я даже наткнулся на интерпретацию IoC как Injection of Container (инъекцию контейнера). Кто-то всерьез предполагает, что существует механизм инъекции контейнеров, похожий на DI … Хм.
Ричард Фейнман был прекрасным рассказчиком, способным ясно и доходчиво объяснять сложные вещи (см., например, это видео). Джоэл Спольски считает, что по-настоящему умный программист обязательно должен уметь говорить на языке непрофессионала (а не только на языке программирования C). И почти всем известен афоризм Альберта Эйнштейна: «Если вы не можете что-то объяснить шестилетнему ребенку, значит, вы сами этого не понимаете». Конечно, я не сравниваю вас с шестилетним ребенком, но я постараюсь рассказать о DI, IoC и другом DI наиболее ясно и внятно.
Инверсия контроля
Что вы обычно делаете в свой выходной? Может, вы читаете книги. Может, вы играете в видеоигры. Может быть, вы пишете код, а может, пьете пиво во время просмотра какого-нибудь сериала (вместо того, чтобы сажать яблони на Марсе). Но что бы вы ни делали, в вашем распоряжении целый день, и вы единолично контролируете свое расписание.
К сожалению, выходные заканчиваются, наступает понедельник, и вам нужно идти на работу (при условии, что у вас есть работа). По условиям трудового договора вы должны быть на работе с 8 утра. Работаете до полудня. Затем у вас перерыв на обед, а затем еще четыре часа лихорадочной активности. Наконец, в 17:00 вы выходите из офиса и отправляетесь домой, где можете снова расслабиться и взять немного пива. Чувствуете разницу? Вы больше не контролируете свой распорядок дня. Это делает кто-то другой — ваш работодатель.
Давайте посмотрим на другой пример. Предположим, вы пишете приложение с текстовым интерфейсом. В своей функции Main вы запрашиваете ввод пользователя, ждете последовательности символов от пользователя, вызываете подпрограммы для обработки полученных данных (возможно, даже в отдельных потоках) и запрашиваете общие функции из подключенных библиотек. Таким образом, все полномочия сосредоточены в ваших руках, а написанный вами код управляет потоком выполнения приложения. Но в один прекрасный день к вам в офис заходит начальник с неприятной новостью — консоли вышли из моды, миром правят графические интерфейсы, и вам придется все переделывать. Будучи современным и гибким программистом (я не имею ввиду ваши занятия йогой), вы сразу же начинаете вносить изменения. Для этого вы подключаете среду графического интерфейса пользователя и пишете код обработки событий. И теперь вы предполагаете, что если нажать эту кнопку, то должно произойти то или иное. А если пользователь меняет свой выбор в выпадающем списке, то будет то и то. И вроде бы все хорошо. Но потом понимаешь, что что-то не так. Кто то другой вызывает эти обработчики событий, которые вы так старательно программируете. Что кем то другим определяется, что произойдет когда пользователь что то нажимает. Что тут происходит? Фреймворк GUI оказался сложнее, чем вы думали, и перехватил у вас контроль над потоком выполнения приложения.
Это называется Инверсия управления (Inversion of Control) — абстрактный принцип, который указывает, что поток выполнения контролируется внешней сущностью. Концепция IoC тесно связана с идеей фреймворка. IoC — это основное различие между фреймворком и другой формализацией повторно используемого кода — библиотекой — набором функций, которые вы просто вызываете из своей программы. Фреймворк — это оболочка, которая предоставляет предопределенные точки расширения. Вы можете вставить свой собственный код в эти точки расширения, но фреймворк определяет, когда этот код будет вызван.
В качестве домашнего задания подумайте, почему Джефф Сазерленд настаивает на том, что SCRUM — это структура, а не методология.
Инверсия зависимостей
Инверсия зависимостей (Dependency inversion) — это буква D в аббревиатуре SOLID. Принцип гласит:
Это определение немного сбивает с толку, поэтому давайте рассмотрим пример (я буду использовать C # для примеров).
Проблема здесь в том, что класс Foo зависит от конкретного класса Bar. По той или иной причине — ради расширяемости, повторного использования или тестирования — вам, возможно, придется самому разделить их. Согласно принципу инверсии зависимостей (Dependency Inversion), вы должны ввести промежуточную абстракцию между ними.
На диаграмме UML графически показаны оба варианта.
Проблема возникает, когда вы спрашиваете, где же здесь реальная инверсия? Основная идея, которая позволяет нам ответить на этот вопрос, заключается в том, что интерфейсы принадлежат не их реализациям, а их клиентам. Название интерфейса, IBar, сбивает с толку и заставляет нас видеть пару IBar + Bar как единое целое. Но истинным владельцем IBar является класс Foo, и если вы примете это во внимание, то направление связи между Foo и Bar действительно меняется.
Внедрение зависимости
Глядя на полученный код, внимательный читатель заметит, что, несмотря на введение промежуточной абстракции, класс Foo по-прежнему отвечает за создание экземпляра класса Bar. Очевидно, это не то разделение, которого мы ожидали.
Чтобы избавить класс Foo от этой неприятной обязанности, было бы неплохо переместить код создания экземпляра в другое место и инкапсулировать его туда (поскольку мы все чрезвычайно прагматичны и не любим ничего писать дважды). Это можно сделать двумя способами — с помощью шаблона Service Locator или Dependency Injection (внедрения зависимостей).
Service Locator — это реестр абстракций и соответствующих реализаций. Вы скармливаете ему интересующий вас интерфейс и получаете новый экземпляр определенного класса. Выглядит это так:
Нюанс в том, что теперь класс Foo никак не зависит от класса Bar, но по-прежнему контролирует его создание. Как мы уже знаем, этого можно избежать, инвертируя поток управления, что означает передачу управления в руки внешнего механизма. Инъекция зависимостей (Dependency Injection) — это тот самый механизм, который реализован во фреймворках, называемых IoC-контейнерами:
Заключение
Честно говоря, IoC-контейнер (IoC-container) — такое дурацкое название, что хуже придумать сложно. Этот термин ничего не говорит о том, что он на самом деле делает, ежедневно сбивая с толку десятки новых программистов. Абсолютно любой фреймворк можно назвать IoC-контейнером, потому что он по определению реализует инверсию управления и является контейнером для кода общего назначения. Этот термин был (и остается) настолько ужасным, что Мартин Фаулер изобрел другой — внедрение зависимостей (Dependency Injection).
Подведем итоги. Мы используем Инверсию зависимостей (Dependency Inversion) для разделения модулей по абстракции а Внедрение зависимостей (Dependency Injection), для того чтобы исключить создание экземпляров вручную (то есть экземпляр создается как бы снаружи а не внутри). Мы реализуем все это через какой-нибудь фреймворк по принципу Инверсии управления (Inversion of Control). И ни один из них не является синонимом, поэтому IoC-контейнеры — яркий пример того, как можно всех запутать одним несчастливым термином. Надеюсь, в этом небольшом опусе мне удалось прояснить разницу между этими понятиями, и что вы больше никогда их не перепутаете. А если кто-то из ваших коллег запутается, вы сможете четко и доходчиво объяснить разницу.
Шаблон Dependency Injection
Внедрение зависимостей — это шаблон проектирования, который применяет принцип IoC, чтобы гарантировать, что класс не имеет абсолютно никакого участия или осведомленности в создании или времени жизни объектов, используемых его конструктором или переменными экземпляра — «общая» забота о создании объекта и заполнении переменных экземпляра вместо этого перекладывается на фреймворк.
То есть класс может указывать свои переменные экземпляра, но не выполняет никакой работы по заполнению этих переменных экземпляра (за исключением использования параметров конструктора в качестве «сквозного»)
Класс, разработанный с учетом внедрения зависимостей, может выглядеть так:
В этом примере Meow и Woof — это зависимости, внедренные через конструктор Foo.
С другой стороны, класс Foo, используемый без внедрения зависимостей, может просто создать сами экземпляры Meow и Woof или, возможно, использовать тот же ServiceLocator или фабрику сервисов:
Таким образом, внедрение зависимостей просто означает, что класс отложил ответственность за получение или предоставление своих собственных зависимостей; вместо этого эта ответственность лежит на том, кто хочет создать экземпляр. (Обычно это контейнер IoC)
Инверсии зависимостей (Dependency Inversion)
Инверсия зависимостей или точнее принцип инверсии зависимостей (Dependency Inversion Principle — DIP) в широком смысле касается отделения конкретных классов друг от друга, предотвращая прямую ссылку этих классов друг на друга.
DIP в первую очередь заботится о том, чтобы класс зависел только от абстракций более высокого уровня. Например, интерфейсы существуют на более высоком уровне абстракции, чем конкретный класс.
DIP не касается внедрения зависимостей, хотя шаблон внедрения зависимостей является одним из многих методов, которые могут помочь обеспечить уровень косвенного обращения, необходимый, чтобы избежать зависимости от деталей низкого уровня и связи с другими конкретными классами.
Инверсия зависимостей часто более явна в статически типизированных языках программирования, таких как C # или Java, поскольку эти языки требуют строгой проверки типов для имен переменных. С другой стороны, инверсия зависимостей уже пассивно доступна в динамических языках, таких как Python или JavaScript, потому что переменные на этих языках не имеют каких-либо ограничений типа.
Рассмотрим сценарий на языке со статической типизацией, где классу требуется возможность читать запись из базы данных приложения:
В приведенном выше примере, несмотря на использование Dependency Injection, класс Foo по-прежнему жестко зависит от SqlRecordReader, но единственное, что его действительно волнует, это то, что существует метод с именем readAll (), который возвращает некоторые записи.
Рассмотрим ситуацию, когда запросы к базе данных SQL позже реорганизуются в отдельные микросервисы, требующие изменения кодовой базы; вместо этого классу Foo потребуется читать записи из удаленной службы. Или, в качестве альтернативы, ситуация, когда модульные тесты Foo должны считывать данные из хранилища в памяти или плоского файла.
Если, как следует из названия, SqlRecordReader содержит базу данных и логику SQL, для любого перехода к микросервисам потребуется изменение класса Foo.
Рекомендации по инверсии зависимостей предполагают, что SqlRecordReader следует заменить абстракцией более высокого уровня, которая предоставляет только метод readAll () то есть использовать интерфейс:
Согласно DIP, IRecordReader является абстракцией более высокого уровня, чем SqlRecordReader, и принуждение Foo зависеть от IRecordReader вместо SqlRecordReader соответствует рекомендациям DIP.
Так всё-таки, что же такое Inversion of Control?
Автор: Бучельников Игорь Владимирович
Опубликовано: 30.11.2012
Исправлено: 10.12.2016
Версия текста: 1.0
Вступление
В очередной раз, поймав себя на мысли, что я не понимаю, что такое Inversion of Control, я решил разобраться. Техническая реализация (Inversion of Control Container и шаблон проектирования «Factory Method») была понятна. Я даже сделал свою реализацию Inversion of Control Container и успешно её использовал в реальных проектах. Но абстрактная теоретическая основа оставалась не постигнутой. А именно хотелось получить ответы на следующие вопросы:
Статьи, найденные в интернете, в основном описывали технику реализации, которая и так была понятна. Теоретические описания даны недостаточно формально и (или) дают определение Inversion of Control не напрямую, как фундаментальное понятие, а косвенным образом через другие известные концепции. Встречаются таинственные заявления типа:
Единственную достаточно качественную попутку формализовать Inversion of Control я нашел в этой статье. Но в ней, на мой взгляд, формализуется событийная модель, а не Inversion of Control.
Сокращения
Inversion of Control ― инверсия потока управления
IoCContainer
Inversion of Control Container
Терминология
Компонент
Контейнер для методов или для других компонентов. В Visual Studio это могут быть класс(ы) или проект(ы).
Система
Множество связанных компонентов. Связь здесь означает, что компонент А зависит от компонента Б.
Зависимость компонентов
Предположим, что компонент А зависит от компонента Б. В этом случае А не может работать без Б. Но Б может работать без А. Метод А может вызвать метод Б, но метод Б не может вызвать метод А. В Visual Studio зависимостью двух компонентов может являться ссылка одного проекта на другой.
Разделяемый компонент
ПРИМЕЧАНИЕ Высокая связанность системы увеличивает суммарный межкомпонентный интерфейс системы (сумму межкомпонентных интерфейсов всех компонентов). Межкомпонентный интерфейс — это обязательства (контракт (протокол), по которому взаимодействуют компоненты), которые должны соблюдать программисты, реализующие компоненты. А чем меньше этих обязательств, тем меньше трудозатрат требуется на их обеспечение, и тем меньше трудозатрат потребуется в случае изменения этих обязательств при изменении требований к системе. Точка входаМетод компонента системы ( подсистемы ), при вызове которого система ( подсистема ) выполняет возложенные на неё функции (этот метод вызывает по цепочке все остальные методы всех компонентов системы ( подсистемы ), в свою очередь, этот метод ни один другой метод системы ( подсистемы ) не вызывает). ПодсистемаНадсистемаСуперсистемаПодкомпонентПоток управления (Control)Аспект масштабирования системыСоглашенияОбозначенияUML диаграммы классов и листинги программного кодаЛистинги программного кода не являются образцом совершенства. Некоторые несущественные технические детали могут быть опущены в целях акцентирования внимания на сути. Код можно считать C#-образным псевдокодом. Теоретическая частьРабочий примерДля примера будем рассматривать программу для печати графиков биржевых индексов РТС и ММВБ: Графики очень похожи. Отличаются только данные и легенда. Предположим, перед нами стоит задача получить на выходе два ярлыка на рабочем столе, при запуске первого, без лишних вопросов пользователю, на принтер отправляется график ММВБ за сессию прошлого дня, при запуске второго то же самое происходит с графиком РТС. У этой системы должно быть предусмотрено 2 аспекта масштабирования : Прямой поток управленияИнверсия потока управленияОпишу, как работает данная архитектура. Вот то, что находится в Принтер. Main(args : string[]) : Замечу, что количество разделяемых компонентов не изменилось.
|