Инверсия и внедрение зависимостей

Spread the love

Перевод: Distillery TechInversion and Injection of Dependencies

В своей работе в 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 # для примеров).

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

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

public class Foo {
  private IBar itsBar;

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

На диаграмме UML графически показаны оба варианта.

Image for post

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

Image for post

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

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

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-container) — такое дурацкое название, что хуже придумать сложно. Этот термин ничего не говорит о том, что он на самом деле делает, ежедневно сбивая с толку десятки новых программистов. Абсолютно любой фреймворк можно назвать IoC-контейнером, потому что он по определению реализует инверсию управления и является контейнером для кода общего назначения. Этот термин был (и остается) настолько ужасным, что Мартин Фаулер изобрел другой — внедрение зависимостей (Dependency Injection).

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

Прим. переводчика: далее оригинальная статья заканчивается. Но мне захотелось ее расширить повторением определений основных терминов Dependency Injection и Dependency Inversion, взятыми с этой страницы StackOverflow .

Шаблон Dependency Injection

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

То есть класс может указывать свои переменные экземпляра, но не выполняет никакой работы по заполнению этих переменных экземпляра (за исключением использования параметров конструктора в качестве «сквозного»)

Класс, разработанный с учетом внедрения зависимостей, может выглядеть так:

// Dependency Injection Example...

class Foo {
    // Constructor uses DI to obtain the Meow and Woof dependencies
    constructor(fred: Meow, barney: Woof) {
        this.fred = fred;
        this.barney = barney;
    }
}

В этом примере Meow и Woof — это зависимости, внедренные через конструктор Foo.

С другой стороны, класс Foo, используемый без внедрения зависимостей, может просто создать сами экземпляры Meow и Woof или, возможно, использовать тот же ServiceLocator или фабрику сервисов:

// Example without Dependency Injection...

class Foo {
    constructor() {
        // a 'Meow' instance is created within the Foo constructor
        this.fred = new Meow();

        // a service locator gets a 'WoofFactory' which in-turn
        // is responsible for creating a 'Woof' instance.
        // This demonstrates IoC but not Dependency Injection.
        var factory = TheServiceLocator.GetWoofFactory();
        this.barney = factory.CreateWoof();
    }
}

Таким образом, внедрение зависимостей просто означает, что класс отложил ответственность за получение или предоставление своих собственных зависимостей; вместо этого эта ответственность лежит на том, кто хочет создать экземпляр. (Обычно это контейнер IoC)

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

Инверсия зависимостей или точнее принцип инверсии зависимостей (Dependency Inversion Principle — DIP) в широком смысле касается отделения конкретных классов друг от друга, предотвращая прямую ссылку этих классов друг на друга.

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

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

Инверсия зависимостей часто более явна в статически типизированных языках программирования, таких как C # или Java, поскольку эти языки требуют строгой проверки типов для имен переменных. С другой стороны, инверсия зависимостей уже пассивно доступна в динамических языках, таких как Python или JavaScript, потому что переменные на этих языках не имеют каких-либо ограничений типа.

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

// class Foo depends upon a concrete class called SqlRecordReader.

class Foo {
    reader: SqlRecordReader;

    constructor(sqlReader: SqlRecordReader) {
        this.reader = sqlReader;
    }

    doSomething() {
        var records = this.reader.readAll();
        // etc.
    }
}

В приведенном выше примере, несмотря на использование Dependency Injection, класс Foo по-прежнему жестко зависит от SqlRecordReader, но единственное, что его действительно волнует, это то, что существует метод с именем readAll (), который возвращает некоторые записи.

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

Если, как следует из названия, SqlRecordReader содержит базу данных и логику SQL, для любого перехода к микросервисам потребуется изменение класса Foo.

Рекомендации по инверсии зависимостей предполагают, что SqlRecordReader следует заменить абстракцией более высокого уровня, которая предоставляет только метод readAll () то есть использовать интерфейс:

interface IRecordReader {
    Records[] getAll();
}

class Foo {
    reader: IRecordReader;

    constructor(reader: IRecordReader) {
        this.reader = reader;
    }
}

Согласно DIP, IRecordReader является абстракцией более высокого уровня, чем SqlRecordReader, и принуждение Foo зависеть от IRecordReader вместо SqlRecordReader соответствует рекомендациям DIP.

Была ли вам полезна эта статья?
[9 / 3.7]

Spread the love
Подписаться
Уведомление о
guest
0 Комментарий
Inline Feedbacks
View all comments
0
Будем рады вашим мыслям, пожалуйста, прокомментируйте.x
()
x