Конфигурируем Ruby модуль
Я думаю вы знакомы с методом configure, который многие гемы предоставляют для конфигурации. Например конфигурация carrierwave: CarrierWave.configure do |config| config.storage = : file config.enable_processing = false end Как реализовать это в своем модуле? Начнем с падающих тестов. # configure.rb require 'minitest/autorun'
class ConfigurationTest < MiniTest::Test def test_configure_block MyModule.configure do |config| config.name = "TestName" config.per_page = 25 end
assert_equal «TestName», MyModule.config.name assert_equal 25, MyModule.config.per_page
assert_equal «TestName», MyModule.config[: name] assert_equal 25, MyModule.config[: per_page] end end ➜ Projects ruby configure.rb Run options: --seed 25758
# Running:
E
Finished in 0.001166s, 857.6329 runs/s, 0.0000 assertions/s.
1) Error: ConfigurationTest#test_configure_block: NameError: uninitialized constant ConfigurationTest: MyModule configure.rb:5: in `test_configure_block'
1 runs, 0 assertions, 0 failures, 1 errors, 0 skips Теперь, когда у нас есть падающие тесты приступим к реализации функциональности. Прежде всего объявим модуль, содержащий метод configure. module MyModule def self.configure
end end Нам нужно место для хранения нашей конфигурации. Я думаю переменная модуля хорошо подойдет для этого. module MyModule def self.configure self.config ||= {} end
def self.config @config end
private
def self.config=(value) @config = value end end Здесь есть проблема. Мы не сможем хранить конфигурацию в хеше. Пока я заменю хеш на OpenStruct, который соответствует функциональности, которую мы собираемся получить в конечном счете. После этого я уже могу вызвать блок внутри метода и передать ему хранилище в качестве аргумента. require 'minitest/autorun' require 'ostruct'
module MyModule def self.configure self.config ||= OpenStruct.new yield (self.config) end
def self.config @config end
private
def self.config=(value) @config = value end end Нужная функциональность готова. Тесты проходят. ➜ Projects ruby configure.rb Run options: --seed 8967
# Running:
.
Finished in 0.001607s, 622.2775 runs/s, 2489.1101 assertions/s.
1 runs, 4 assertions, 0 failures, 0 errors, 0 skips
Пришло время провести рефакторинг этого решения. Сходу видны две проблемы: Мы можем хранить что угодно внутри нашей конфигурации. Набор методов которым мы можем передать значение ничем не ограничен. Это не круто для конфигурации, потому что это прячет ошибки от пользователя. Если пользователь совершит ошибку в названии конфигурационного метода, мы должны немедленно дать ему знать об этом, выбросив исключение. OpenStruct не очень хорошая идея для продакшн-кода. Он намного медленнее чем обычный Struct или класс и использует намного больше памяти. Добавим тесты, чтобы быть увереными, что при вызове несуществующего конфигурационного методы мы получим исключение. def test_set_not_exists_attribute assert_raises NoMethodError do MyModule.configure do |config| config.unknown_attribute = «TestName» end end end
def test_get_not_exists_attribute assert_raises NoMethodError do MyModule.config.unknown_attribute end end У нас есть два способа исправить это. Первый — использовать Struct с белым списком доступных конфигурационных методов. module MyModule CONFIG_ATTRIBUTES = %i (name per_page)
def self.configure self.config ||= Struct.new (*CONFIG_ATTRIBUTES).new yield (self.config) end
def self.config @config end
private
def self.config=(value) @config = value end end Все выглядит отлично. Тесты проходят, код простой и читаемый. Но я забыл одну важную деталь. Конфигурационные значения по-умолчанию. Для них нужно добавить еще один тест. def test_default_values MyModule.configure do |config| config.name = «TestName» end
assert_equal 10, MyModule.config.per_page end Чтобы избежать перезаписывания конфигурационных значений в разных тестах нужно добавить сброс предыдущей конфигурации перед запуском каждого теста. Я добавлю метод сброса прямо в тестовом классе, потому что он нужен только для тестовых нужд и нет необходимости делать его частью публичного API. module: MyModule def self.reset self.config = nil end end
def setup MyModule.reset end Вернемся к решению проблемы со значениями по-умолчанию. Простейшее решение будет выглядеть так: self.config ||= begin config = Struct.new (*CONFIG_ATTRIBUTES).new config.per_page = 10 config end Хм, код начинает попахивать. Значения по-умолчанию могут быть намного сложнее. Такой код будет сложно поддерживать. Я думаю мы можем сделать лучше. Давайте заменим Struct на класс. В классе мы можем устанавливать значения по-умолчанию прямо в инициализаторе. Такой код будет легко читать и расширять. module MyModule class Configuration attr_accessor: name, : per_page
def initialize @per_page = 10 end
def [](value) self.public_send (value) end end
def self.configure self.config ||= Configuration.new yield (self.config) end
def self.config @config end
private
def self.config=(value) @config = value end end Мне нравится это решение. Оно все еще очень простое и читаемое. Оно также достаточно гибкое. Мы можем устанавливать сложные значения по-умолчанию и при необходимости выносить их в отдельные методы. Мы также имеем два способа получать конфигурационные значения: с помощью метода и через subscript.Это все, чем я хотел поделиться сегодня. Исходники доступны здесь: goo.gl/feCwCC