Паттерн «Конфигурирование Модуля» в Ruby
Обсудим паттерн «конфигурирования модуля» а так же его характерные черты, приемы реализации и тест-кейсы (требования).
Для начала придумаем наш сферический модуль в ваккууме, какой делает что-нибудь полезное.
В учебном примере не важно, что модуль делает конкретно. Мы не будем писать его функционал, а применим прием (паттерн) конфигурирования модуля (или гема).
Он существует в несколько разных вариантах и реализациях, но это нормально для Ruby с его свободой самовыражения.
Назовем наш модуль MomentalPush, допустим он каким-то образом посылает сообщения пользователям в соцсети.
Конечно, хорошо бы иметь возможность его красиво настроить.
Допустим, у модуля есть настройки: в какие соцсети посылать сообщения, а так же какой email использовать для отправки сообщений по почте, помимо соцсетей.
Стандарт де-факто в конфигурировании гемов и отдельных модулей это применение блока.
Вот так:
Безусловно, количество таких настроек зависит от размера и задач Вашего модуля. Он может требовать две-три, но может и гораздо больше, например 10 настроечных параметров.
Итак, за дело. Я буду использовать тест-инструмент Rspec.
Выработаем самые простые требования к модулю:
Он должен:
- Существовать вообще
- Иметь метод configure
- При присвоении настроечных переменных хранить их и при обращении к конфигурации — возвращать
- Хорошо бы иметь настройки по умолчанию если пользователь ввел их не все, или пропустил обязательные
- Если программист ошибся при настройке и ввел параметр какой в нашем модуле не предусмотрен, нужно чтобы он немедленно получил
лещаисключение. Это очень важный момент! Многие упускают его и их решения не гибки — можно вводить в блоке конфигурации какую угодно белиберду и она «молча срабатывает» что потом вызывает трудности при отладке, ловле ошибок если вместо name ошиблись и ввели настройку с названием names например.
Создадим тест на базе этих требований:
Тест конечно можно еще отрефакторить, нет предела совершенству. Но он достаточно неплох.
Обратите внимание — тестируются типы данных с помощью shared_examples, устранено повторение кода путем общих let-деклараций. Для удобства тестирования (это не обязательно!) введена возможность в решение ресета конфигурации MomentalPush.reset Так же, проверяется случай когда программист вводит какую-нибудь неизвестную модулю переменную.
А теперь код самого решения, собственно сам паттерн «конфигурация модуля»:
Что тут нужно пояснить? Во-первых трюк с class << self просто сделан из-за того что модуль не может иметь наследников, но может быть «сам по себе» синглетом. То есть к модулю можно обращаться. Но если бы мы объявили аксесор без раскрытия модуля «на себя» как синглета, то аксесор имели бы классы и обьекты какие примешиваются к модулю, а не он сам статично. По этому используется трюк «раскрытие синглета на себя».
Далее — статик методы конфигурации лениво возвращают конфигурацию (экземпляр встроенного в модуль класса Config к какому с помощью трюка дан доступ извне для обращений) и вызывают yield выброс назад конфигурации в контекст блока конфигурирования.
Так как класс Config имеет четко известные поля и их значения по умолчанию, то модуль соответственно выдает как и нужно в тестовых случаях, NoMethodError исключение при неизвестном параметре так как неизвестный параметр «за кулисами» это обращение к несуществующему методу класса Config. Что нам и нужно.
Есть и альтернативные способы реализации, например без трюка на раскрытие:
Хоть код более читаем и понятен, а вместо аксесора и трюка используется статик переменная модуля, мне больше нравится первый вариант. Хотя это на усмотрение разработчика!
Ruby создан для самовыражения, по этому даже паттерн или прием могут реализоваться по разному.