Статьи

SOLID Ruby: принцип обращения зависимостей

Продолжая нашу серию, давайте поговорим о принципе инверсии зависимостей (DIP) . Этот принцип гласит, что программные компоненты должны зависеть от абстракций, а не от реализаций. Что это значит?

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

Но как насчет Руби? Это динамический язык, и нам не нужно указывать типы зависимостей (утка — единственная лучшая особенность, которую может иметь язык, IMHO), поэтому мы можем использовать DIP бесплатно, верно? Хорошо, если вы примените несколько простых техник, да. Давай поговорим об этом.

Давайте вернемся к примеру из предыдущей статьи:

class Game < ActiveRecord::Base
belongs_to :category
validates_presence_of :title, :category_id, :description,
:price, :platform, :year
end
 
class GamePriceService
attr_accessor :game
 
# we could use a config file
BASE_URL = «http://thegamedatabase.com/api/game»
API_KEY = «ek2o1je»
 
def initialize(game)
self.game = game
end
 
def get_price
data = open(«#{BASE_URL}/#{game.name}/price?api_key=#{API_KEY}«)
JsonParserLib.parse(data)
end
end
 
class GamePrinter
attr_accessor :game
 
def initialize(game)
self.game = game
end
 
def print
price_service = GamePriceService.new(game)
<<-EOF
#{game.name}, #{game.platform}, #{game.year}
current value is #{price_service.get_price[:value]}
EOF
end
end

view raw
gistfile1.rb
hosted with ❤ by GitHub

Этот код имеет жесткие зависимости. Мы вызываем классы GamePriceService и JsonParserLib явно из GamePrinter # print и GamePriceService # get_price, соответственно. Как вы можете видеть, даже если типизированная утка позволяет нашему коду быть независимым от типа, мы написали код таким образом, чтобы наши классы были привязаны к определенным типам через его зависимости.

Хороший способ в полной мере воспользоваться преимуществами утки и получить все преимущества DIP — сделать наши зависимости прозрачными. Ruby делает это довольно просто:

class Game < ActiveRecord::Base
belongs_to :category
validates_presence_of :title, :category_id, :description,
:price, :platform, :year
end
 
class GamePriceService
attr_accessor :game, :json_parser
 
# we could use a config file
BASE_URL = «http://thegamedatabase.com/api/game»
API_KEY = «ek2o1je»
 
def initialize(game, json_parser = JsonParserLib)
self.game = game
self.json_parser = json_parser
end
 
def get_price
data = open(«#{BASE_URL}/#{game.name}/price?api_key=#{API_KEY}«)
json_parser.parse(data)
end
end
 
class GamePrinter
attr_accessor :game, :game_webservice
 
def initialize(game, game_webservice = GamePriceService)
self.game = game
self.game_webservice = game_webservice
end
 
def print
price_service = game_webservice.new(game)
<<-EOF
#{game.name}, #{game.platform}, #{game.year}
current value is #{price_service.get_price[:value]}
EOF
end
end
 
# Usage example:
game = Game.new(some_game_data)
webservice = GamePriceService.new(game)
game.price = webservice.get_price
 
GamePrinter.new(game).print

view raw
gistfile1.rb
hosted with ❤ by GitHub

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

Этот метод называется «инъекция зависимости». Несмотря на схожие названия, его не следует путать с DIP — инъекция — это один из способов его достижения, а не единственный.

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

class Game < ActiveRecord::Base
belongs_to :category
validates_presence_of :title, :category_id, :description,
:price, :platform, :year
end
 
class GamePriceService
attr_accessor :game
 
# we could use a config file
BASE_URL = «http://thegamedatabase.com/api/game»
API_KEY = «ek2o1je»
 
def initialize(game)
self.game = game
end
 
def get_price(json_parser = JsonParserLib)
data = open(«#{BASE_URL}/#{game.name}/price?api_key=#{API_KEY}«)
json_parser.parse(data)
end
end
 
class GamePrinter
attr_accessor :game
 
def initialize(game)
self.game = game
end
 
def print(game_webservice = GamePriceService)
price_service = game_webservice.new(game)
<<-EOF
#{game.name}, #{game.platform}, #{game.year}
current value is #{price_service.get_price[:value]}
EOF
end
end
 
# Usage example:
game = Game.new(some_game_data)
webservice = GamePriceService.new(game)
game.price = webservice.get_price
 
GamePrinter.new(game).print

view raw
gistfile1.rb
hosted with ❤ by GitHub

Делая это, мы избегаем конструкторов со слишком большим количеством параметров, когда это становится проблемой. Обычно я выбираю внедрение конструктора, когда существование класса не имеет смысла без зависимости (например, внедрение объекта с поддержкой HTTP в класс, который использует API), и выбираю внедрение метода, когда использование зависимости ограничено сам метод (например, внедрение объекта User в метод, которому требуется имя пользователя для отправки электронной почты). Хотя это субъективно, так что делай так, как тебе лучше

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

Говоря о тестировании, это контекст, в котором DIP действительно сияет. Теперь, когда у нас есть возможность вставлять зависимости в наш код, использование тестовых двойников (фальшивых объектов, заменяющих реальные, таких как заглушки, насмешки и шпионы) тривиально. Хорошим побочным эффектом является скорость выполнения теста: поскольку мы можем использовать double вместо реальных зависимостей, время установки и ожидания должно быть намного меньше.

В сторону: тест удваивается

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

  • Реальный код медленный или использует внешние ресурсы, которые не всегда доступны;
  • Реальный код зависит от службы, которая не имеет средств тестирования (например, платежный шлюз без песочницы);
  • Вы хотите определить поведение посредством взаимодействия (проверки протоколов), написания модульных тестов, которые действительно изолированы.

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

  • Заглушки : они используются для замены вызова метода предопределенным возвращаемым значением. Это самый простой вариант использования;
  • Насмешки : ответственные за «настоящую» магию BDD, они как заглушки, которые также могут записывать взаимодействия и полученные сообщения. Затем, после выполнения теста, они могут проверить, были ли получены указанные сообщения (включая количество раз, когда сообщение было получено, и аргументы, переданные);
  • Шпионы : они похожи на насмешки, разница здесь в том, что с помощью насмешек вы пишете ожидание, а затем вызываете код, который должен (или не должен) его запускать. С помощью шпионов вы поддерживаете «нормальный» поток тестирования, вызывая код и затем проверяя взаимодействия (например, традиционные утверждения);
  • Подделки : более простые версии сложных объектов, как правило, «созданные вручную» (без использования специального инструмента или инфраструктуры) и используемые, когда реальный код приводит к слишком медленным, громоздким или ненадежным тестам.

Вернемся к нашей основной теме, вот простой пример спецификации с использованием этой техники:

# WARNING: pseudo code
class Authenticator
def initialize(user_repository = User)
@user_repository = user_repository
end
def authenticate(identifier, hashed_password)
@user_repository.find(:username => identifier, :password => hashed_password).present?
end
end
describe Authenticator
let(:repo) { double(«user repo») }
let(:authenticator) { Authenticator.new(repo) }
context «with invalid credentials»
before { repo.stub(:find => nil) }
it «returns false» do
authenticator.authenticate(«invalid», «invalid»).should_not be
end
end
context «with valid credentials»
before { repo.stub(:find => double) }
it «returns true» do
authenticator.authenticate(«valid», «valid»).should be
end
end
end

view raw
gistfile1.rb
hosted with ❤ by GitHub

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

Подводим итоги: три на одного с принципом открытого и закрытого типа, принципом подстановки Лискова и принципом разделения интерфейсов.

Дополнительные ссылки:

Сделайте ваши зависимости прозрачными с параметрами по умолчанию

Тест удваивается в Википедии

Издевательства Мартина Фаулера