- PVSM.RU - https://www.pvsm.ru -
Я думаю вы знакомы с методом 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
Пришло время провести рефакторинг этого решения. Сходу видны две проблемы:
Добавим тесты, чтобы быть увереными, что при вызове несуществующего конфигурационного методы мы получим исключение.
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 [1]
Автор: BloodyHistory
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ruby/73956
Ссылки в тексте:
[1] goo.gl/feCwCC: http://goo.gl/feCwCC
[2] Источник: http://habrahabr.ru/post/242729/
Нажмите здесь для печати.