- PVSM.RU - https://www.pvsm.ru -
На Хабре уже была статья [2], посвящённая Dependency Injection в Ruby, но упор в ней был больше на использование паттерна IoC-container с помощью гемов dry-container [3] и dry-auto_inject [4]. А ведь для использования преимуществ внедрения зависимостей совершенно необязательно городить контейнеры или подключать библиотеки. Сегодня расскажу о том, как по-быстрому реализовать DI своими руками.
Для чего люди используют DI? Обычно для того, чтобы во время тестов менять поведение кода, избегая вызовов ко внешним сервисам или просто для тестирования объекта в изоляции от окружения. Конечно, DHH говорит, что мы можем застабить Time.now
[5], и наслаждаться зелёными точками тестов без лишних телодвижений, но не стоит слепо верить всему, что говорит DHH. Лично мне больше нравится точка зрения Piotr Solnica, изложенная в этом посте [6]. Он приводит такой пример:
class Hacker
def self.build(layout = 'us')
new(Keyboard.new(layout: layout))
end
def initialize(keyboard)
@keyboard = keyboard
end
# stuff
end
Параметр keyboard
в конструкторе — и есть внедрение зависимости. Подобный подход позволяет тестировать класс Hacker
, передавая вместо реального инстанса Keyboard
моки. Изоляция, все дела:
describe Hacker do
let(:keyboard) { mock('keyboard') }
it 'writes awesome ruby code' do
hacker = Hacker.new(keyboard)
# some expectations
end
end
Но что мне в нравится примере выше, так это изящный трюк с методом .build
, в котором происходит инициализация keyboard. В обсуждениях DI я видел немало советов, в которых предлагалось инициализацию зависимостей выносить в вызывающий код, например, в контроллеры. Ага, и потом искать по всему проекту вхождения Hacker, чтобы посмотреть, какой конкретно класс используется для клавиатуры, ну-ну. То ли дело .build
: дефолтный usecase на видном месте, ничего не нужно искать.
Рассмотрим следующий пример:
class ExternalService
def self.build
options = Config.connector_options
new(ExternalServiceConnector.new(options))
end
def initialize(connector)
@connector = connector
end
def accounts
@connector.do_some_api_call
end
end
class SomeController
def index
authorize!
ExternalService.build.accounts
end
end
Видно, что контроллер создаёт ExternalService, используя реальные объекты (хоть это и скрыто в методе ExternalService.build
), чего мы стараемся избежать, внедряя DI. Как справиться с этой ситуацией?
Подменять ExternalService.build
. Фактически то, о чём говорил DHH, но есть один важный момент: заменяя .build
, мы не меняем поведение инстансов класса, только обёртку. Пример на RSpec:
connector = instance_double(ExternalServiceConnector, do_some_api_call: [])
allow(ExternalService).to receive(:build) { ExternalService.new(connector) }
Мне кажется, что наиболее эффективным является сочетание второго и третьего подходов: с помощью второго тестируем исключительные ситуации, с помощью третьего убеждаемся в том, что нет ошибок в коде, который инстанцирует объекты.
Несмотря на написанное выше, я не против применения IoC-контейнеров в целом; просто полезно помнить, что существуют альтернативы.
Ссылки, используемые в посте:
Автор: HedgeSky
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ruby/177072
Ссылки в тексте:
[1] Image: https://habrahabr.ru/post/308188/
[2] была статья: https://habrahabr.ru/company/latera/blog/301338/
[3] dry-container: https://github.com/dry-rb/dry-container
[4] dry-auto_inject: https://github.com/dry-rb/dry-auto_inject
[5] застабить Time.now
: http://david.heinemeierhansson.com/2012/dependency-injection-is-not-a-virtue.html
[6] этом посте: http://solnic.eu/2013/12/17/the-world-needs-another-post-about-dependency-injection-in-ruby.html
[7] Источник: https://habrahabr.ru/post/308188/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.