...

понедельник, 10 ноября 2014 г.

Конфигурируем 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


This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий