DIY DI в Ruby


На Хабре вже стаття, присвячена Dependency Injection в Ruby, але упор в ній був більше на використання патерну IoC-container з допомогою гемов dry-container і dry-auto_inject. Адже для використання переваг впровадження залежностей зовсім необов'язково городити контейнери або підключати бібліотеки. Сьогодні розповім про те, як по-швидкому реалізувати DI своїми руками.
Опис підходу
Для чого люди використовують DI? Зазвичай для того, щоб під час тестів змінювати поведінку коду, уникаючи викликів до зовнішніх сервісів або просто для тестування об'єкта в ізоляції від оточення. Звичайно, DHH говорить, що ми можемо застабить
Time.now
, і насолоджуватися зеленими точками тестів без зайвих рухів тіла, але не варто сліпо вірити всьому, що говорить DHH. Особисто мені більше подобається точка зору Piotr Solnica, викладена в цьому пості. Він наводить такий приклад:
class Hacker
def self.build(layout = 'us')
new(Keyboard.new(layout: layout))
end

def initialize(keyboard)
@keyboard = keyboard
end
# stuff
end

<habracut/>
Параметр
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. Як впоратися з цією ситуацією?
  1. Не тестувати викликає код взагалі. Так собі варіант, вирішив записати його для повноти картини.
  2. Підміняти
    ExternalService.build
    . Фактично те, про що говорив DHH, але є один важливий момент: замінюючи
    .build
    , ми не міняємо поведінка инстансов класу, лише обгортку. Приклад на RSpec:
    connector = instance_double(ExternalServiceConnector, do_some_api_call: [])
    allow(ExternalService).to receive(:build) { ExternalService.new(connector) }

  3. Тестувати контролери з допомогою інтеграційних тестів на CI-сервері. Плюси: тестується production-код, підвищуючи ймовірність того, що потенційний баг відловлять тести, а не користувачі. Мінуси: складніше тестувати виняткові ситуації ("сторонній сервіс впав") і не завжди у сторонніх сервісів є аккаунт-пісочниця, на якому можна без побоювання ганяти тести.
  4. -таки IoC-контейнери.
Мені здається, що найбільш ефективним є поєднання другого і третього підходів: з допомогою другого тестуємо виняткові ситуації, з допомогою третього переконуємося в тому, що немає помилок в коді, який инстанцирует об'єкти.
Висновки
Незважаючи на написане вище, я не проти застосування IoC-контейнерів в цілому; просто корисно пам'ятати, що існують альтернативи.
Посилання, які використовуються в пості:
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.