Во время одного из своих Java-проектов я наткнулся на инъекцию зависимостей, которая играет важную роль в разработке Java. Сначала я действительно не понимал, зачем людям нужна инъекция зависимостей. Я потратил некоторое время и прочитал об инъекции зависимостей и о том, как ее реализовать.

Итак, сегодня я хочу попытаться объяснить внедрение зависимостей и ее преимущества. Также я хочу показать, как реализовать внедрение зависимостей в Ruby с помощью библиотек dry-rb.

Что такое внедрение зависимостей

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

class NotePresenter
  def initialize
    @note_storage = NoteTextStorage.new
  end
  def pending_notes
    notes = @note_storage.get_all
    notes.select { |n| n.pending? }
  end
end

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

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

Второе преимущество состоит в том, что это делает код более тестируемым. При тестировании NotePresenter мы, возможно, не захотим использовать реальную реализацию NoteTextStorage. Мы могли бы создать фиктивную реализацию NoteTextStorage следующим образом:

class TestStorage
  
  def pending_notes
    note1 = Note.new title: 'test', text: 'test', pending: true
    note2 = Note.new title: 'test2', text: 'test2', pending: false
  end
end

и использовать его как хранилище только для тестирования.

Реализация внедрения зависимостей в Ruby

Итак, как мы можем реализовать внедрение зависимостей в Ruby. Один из простых способов - просто передать его в конструктор:

class NotePresenter
  def initialize(storage)
    @note_storage = storage
  end
  def pending_notes
    notes = @note_storage.get_all
    notes.select { |n| n.pending? }
  end
end

Естественно, все объекты хранилища, переданные презентатору, должны реализовывать один и тот же интерфейс, чтобы это работало. Этот способ прост и понятен, и во многих случаях его будет достаточно. Но что, если наше хранилище используется не только нашим NotePresenter, но и другими классами. Нам пришлось бы искать и изменять каждый фрагмент кода, в котором наше решение хранения было передано другому объекту. Во-вторых, это решение не будет работать для фреймворка, где пользователь не должен редактировать код фреймворка. Здесь лучше подошел бы подход, похожий на конфигурацию. Так работает большинство библиотек внедрения зависимостей. У них централизованная конфигурация всех компонентов и способов их склейки. Разработчик может изменить реализации подчеркивания, просто изменив конфигурацию.

Dry-rb предлагает две жемчужины этого подхода: dry-container и dry-auto_inject.

Давайте реализуем наш пример с помощью этих двух драгоценных камней:

dependency_container = Dry::Container.new

Здесь мы создаем экземпляр объекта, который будет служить контейнером нашей конфигурации (склейка компонентов).

dependency_container.register('note_storage', -> { NoteTextStorage.new })

Здесь мы зарегистрировали новую конфигурацию с ключом note_storage и связали ее с нашим решением NoteTextStorage.

AutoInject = Dry::AutoInject(dependency_container)

После завершения нашей конфигурации мы настроили его, используя верхнюю строку кода. Теперь мы готовы внедрить наши зависимости в наши классы.

class NotePresenter
  include AutoInject['note_storage']
  def pending_notes
    notes = note_storage.get_all
    notes.select { |n| n.pending? }
  end
end

Здесь мы вводим нашу конфигурацию note_storage в наш NotePresenter. Благодаря этому экземпляр, переданный в конфигурации, доступен с помощью метода note_storage. Теперь предположим, что мы хотим изменить нашу реализацию хранилища подчеркивания. Все, что нам нужно сделать, это создать новую реализацию и передать класс в нашем контейнере как зависимость note_storage.

Для теста мы просто возьмем другую конфигурацию вроде этой:

dependency_container = Dry::Container.new
dependency_container.register('note_storage', -> { TestStorage.new })
AutoInject = Dry::AutoInject(dependency_container)

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