Разработка → Инверсии зависимостей управления впрыском

Cromathaar 8 февраля в 20:18 13,8k
image

Вступление


Наверняка первый вопрос, который возник у вас при взгляде на заголовок, был "Шта?". На самом деле я просто перевел фразу "Инверсия управления, внедрение зависимости" в Google Translate на китайский, а затем обратно. Зачем? Затем, что на мой взгляд, это хорошая иллюстрация того, что происходит на самом деле. Люди вокруг путают, коверкают и извращают эти понятия. По долгу службы я провожу много интервью, и 90% того, что я слышу, когда задаю вопрос про DI — честно говоря, откровенный бред. Я сделал поиск по Хабру и нашел несколько статей, которые пытаются раскрыть эту тему, но не могу сказать, что они мне сильно понравились (ладно, ладно, я проглядел только три первых страницы, каюсь). Здесь же на Хабре я встречал в комментариях такую расшифровку IoC, как Injection of Container. Кто-то всерьез предполагает, что есть некий механизм инъекции контейнеров, который сосуществует где-то рядом с DI, и, видимо, даже делает нечто похожее. Только с контейнерами. Мда. На самом деле понять внедрение зависимости очень просто, надо всего лишь…

Удивительно, но факт — эта штука со «всего лишь...» действительно работает! Иначе вы бы здесь не оказались, не так ли?

Ричард Фейнман был удивительным рассказчиком, умевшим ясно и доступно объяснять весьма сложные вещи (посмотрите, хотя бы, это видео). Джоэл Спольски считает, что по-настоящему умный программист обязательно должен уметь изъясняться на человеческом языке (а не только на Си). Ну и, наверное, практически каждому известен афоризм Альберта Эйнштейна: "Если вы что-то не можете объяснить шестилетнему ребёнку, вы сами этого не понимаете". Конечно же, я не собираюсь сравнивать вас с шестилетними детьми, но тем не менее постараюсь рассказать про DI, IoC и еще один DI максимально просто и понятно.

NB. А вы знали, что помимо внедрения зависимости через конструктор и сеттер, существует еще и третий способ — внедрение посредством интерфейса? Хоть это и выходит за рамки данной статьи, но, держу пари, что либо вы сейчас открыли для себя кое-что новенькое, либо по крайней мере опустили уже заготовленный тухлый помидор.

Инверсия управления (Inversion of Control)


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

Однако, к сожалению, выходные заканчиваются, наступает понедельник, и приходится идти на работу (если, конечно, она у вас есть). По условиям трудового договора вы должны быть на месте в 8 утра. Вы работаете до полудня. Потом у вас перерыв на обед, а затем еще четыре часа кипучей деятельности. Наконец, в 17:00 вы выбираетесь из офиса и отправляетесь домой, где снова можете расслабиться и вмонтировать пивандрия. Чувствуете разницу? Вы больше не управляете своим дневным расписанием, это делает кое-кто другой — ваш работодатель.

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

Но в один прекрасный день в кабинет входит босс и сообщает пренеприятнейшее известие — консоль больше не в моде, миром правят графические интерфейсы, а значит все надо переделать. Будучи современным и гибким (речь не только о ваших занятиях йогой) программистом, вы сразу принимаетесь за внесение изменений. Для этого вы подключаете GUI-фреймворк и пишете код обработки событий. Если нажата вот эта кнопка, то надо сделать то-то и то-то. А если пользователь изменил свой выбор в выпадающем списке, то не обойтись без вот этого и этого. Все идет хорошо, но тут вы понимаете, что раньше было как-то по-другому. А кто, собственно, вызывает эти обработчики событий, которые вы так усердно программируете? Кто вообще определяет, куда и когда нажал пользователь? Что вообще происходит? Где мои носки? GUI-фреймворк оказался явно хитрее, чем вы думали, и перехватил у вас управление потоком выполнения приложения.

Это и есть Inversion of Control — очень абстрактный принцип, постулирующий факт задания потока выполнения некой внешней по отношению к вам сущностью.

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

В качестве домашнего задания поразмышляйте, почему Джефф Сазерленд настаивает на том, что SCRUM — это именно фреймворк, а не методология.

Инверсия зависимости (Dependency Inversion)


Это та самая буква D в аббревиатуре SOLID — принцип, говорящий о том, что:

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

Немного путаная формулировка, поэтому рассмотрим следующий пример (для примеров я буду использовать C#).

public class Foo {
  private Bar itsBar;

  public Foo() {
     itsBar = new Bar();
  }
}

Проблема здесь заключается в том, что класс Foo зависит от конкретного класса Bar. По той или иной причине — в целях расширяемости, переиспользуемости или тестируемости ради — может встать задача их разделить. Согласно принципу инверсии зависимости для этого следует ввести между ними промежуточную абстракцию.

public class Foo {
  private IBar itsBar;

  public Foo() {
     itsBar = new Bar();
  }
}

Диаграмма UML наглядно демонстрирует оба варианта.

image

Сложности начинаются, когда спрашиваешь, а где же здесь, собственно, инверсия? Основополагающая идея, без понимания которой невозможно ответить на этот вопрос, заключается в том, что интерфейсы принадлежат не своим реализациям, а использующим их клиентам. Имя интерфейса IBar вводит в заблуждение и заставляет рассматривать связку IBar + Bar как единое целое. В то же время истинным владельцем IBar является класс Foo, и если принять это во внимание, то направление связи между Foo и Bar действительно обратится вспять.

image

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


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

public class Foo {
  private IServer itsServer;

  public Foo() {
     itsServer = new Bar();
  }
}

Чтобы избавить класс Foo от такой неприятной обязанности, хорошо было бы вынести код инстантинации куда-то в другое место и инкапсулировать его там (поскольку все мы чрезвычайно прагматичны и не любим ничего писать дважды). Сделать это можно двумя способами — используя либо Service Locator, либо Dependency Injection.

Service Locator — это такой реестр соответствия абстракций и их реализаций. Вы скармливаете ему интересующий вас интерфейс, а в ответ получаете готовый экземпляр конкретного класса. Выглядит это примерно так:

public class Foo {
  private IServer itsServer;

  public Foo() {
     itsServer = ServiceLocator.Resolve<IServer>();
  }
}

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

public class Foo {
  private IServer itsServer;

  public Foo(IServer server) {
     itsServer = server;
  }
}

Заключение


На самом деле IoC-контейнер — настолько дурацкое название, что навскидку даже сложно придумать что-то хуже. Оно совершенно ничего не говорит о том, чем на самом деле занимается, вводя в заблуждение десятки все новых и новых программистов каждый день. Абсолютно любой фреймворк можно назвать IoC-контейнером, так как он по определению реализует инверсию управления и является оболочкой для некоего кода общего назначения. Это термин был (и продолжает быть) настолько отвратительным, что Мартин Фаулер придумал другой — внедрение зависимости.

Подытожим. Мы используем Dependency Inversion, чтобы разделить модули абстракцией, Dependency Injection, чтобы избавиться от инстантинации вручную, а реализуем это посредством фреймворка, построенного по принципу Inversion of Control. И ничто из этого не является синонимами, поэтому IoC-контейнеры — яркий пример того, как можно намертво всех запутать с помощью одного единственного неудачного термина.

Надеюсь, в этом маленьком опусе у меня получилось донести до вас разницу между этими вещами, и вы больше никогда их не перепутаете. А если перепутает кто-то из ваших коллег, то вы сможете просто и понятно объяснить им, в чем они не правы.
Проголосовать:
+34
Сохранить: