Во время одного из своих 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)
Подводя итог: внедрение зависимостей дает нашему коду более слабую связь и, таким образом, большую гибкость для изменений, а также более тестируемый код.