Если вы работаете с Ruby on Rails или пишете тесты RSpec — вы уже используете DSL.
Взгляните на привычные конструкции:
describe User do it 'validates presence of name' do user = User.new(name: nil) expect(user).not_to be_valid endend
class Post < ApplicationRecord belongs_to :author has_many :comments validates :title, presence: trueend
Что общего у этих фрагментов?
Код описывает желаемое поведение, а не алгоритм его реализации.
Мы просто пишем:
-
validates :title, presence: trueпроверяет поля -
it 'does something' описывает сценарий
Этот мини‑язык настолько прочно вошёл в нашу жизнь, что кажется естественным. Признаюсь, когда я начинала изучать Ruby, в том числе Rails, я не придавала значения, как на самом деле работают эти конструкции.
В этой статье на примере конфигурационного файла мы разберем, какие механизмы Ruby позволяют написать лаконичный DSL, вроде validates :title.
Давайте разбираться.
Что такое DSL?
DSL (Domain‑Specific Language) — это узкоспециализированный мини-язык, созданный для решения конкретных задач:
-
использует естественный, читаемый синтаксис;
-
скрывает низкоуровневые детали реализации;
-
позволяет описывать что нужно сделать, а не как это сделать.
Пример config/routes.rb
Rails.application.routes.draw do root "pages#home" get 'about', to: "pages#about" resources :articles get 'signup', to: "users#new" resources :users, except: [:new] get 'login', to: "sessions#new" post 'login', to: "sessions#create" delete 'logout', to: "sessions#destroy" resources :categoriesend
Даже, если вы не знакомы с Ruby, наглядно видно, что здесь описываются доступные в приложении URL-адреса и действия при переходе по ним.
Как это работает?
Взглянем на пример конфигурационного файла. Что здесь происходит?
AppConfig.setup do host '127.0.0.1' port 6380 logging level: :info pool size: 20end
По сути, мы передаем некий набор параметров для конфигурации приложения. Есть ли хоть какой-то намек, как это дальше работает? Нет.
Перейдем к деталям реализации.
Есть некий класс AppConfig c классовым методом setup, куда можно передать блок кода с методами host, port, logging, pool.
Взглянем на этот класс:
require 'singleton'class AppConfig include Singleton def initialize @settings = {} end def host(value) @settings[:host] = value end def port(value) @settings[:port] = value.to_i end def logging(options = {}) @settings[:logging] ||= {} @settings[:logging].merge!(options) end def pool(options = {}) @settings[:pool] ||= {} @settings[:pool].merge!(options) end def show puts "🛠 Текущие настройки:" @settings.each do |k, v| puts " #{k}: #{v.inspect}" end end def self.setup(&block) instance.instance_eval(&block) instance endend
Мы видим методы (host, port, logging, pool), где реализована простая запись параметров в хеш (для примера).
Что означает include Singleton?
Есть такой паттерн проектирования под названием Singleton, гарантирующий, что у класса будет ровно один экземпляр. Это важно как раз для конфигурации приложения, сервера или подключения внешнего сервиса, что гарантирует использование единственной конфигурации.
Singleton в данном случае — это модуль, который мы подключаем к классу и получаем следующее:
-
создание экземпляров через new блокируется
-
единственный экземпляр доступен через метод
instance
# Этот метод уже есть в модуле Singletondef instance @instance ||= newend
Экземпляр класса создается только в том случае, если ранее он не был создан.
Внутри классового метода setup вызываем instance и на нем instance_eval с переданным блоком. Instance_eval выполняет код в контексте переданного объекта (экземпляр класса AppConfig).
Проверяем работоспособность, вызывая метод show на экземпляре класса.
require_relative 'dsl'require_relative 'config'config = AppConfig.instanceconfig.show
🛠 Текущие настройки: host: "127.0.0.1" port: 6380 logging: {:level=>:info} pool: {:size=>20}
Все наши параметры успешно записаны в хеш.
Добавим гибкости c method_missing
Сейчас AppConfig поддерживает только жестко заданные методы (host, port, logging, pool). Но что, если мы хотим добавить новые настройки без добавления новых методов в класс? Для таких целей в Ruby существует метод method_missing.
Исходя из названия, method_missing предназначен для обработки вызовов несуществующих методов. Давайте переопределим его, тк по умолчанию method_missing просто выдает NoMethodError.
# добавляем метод в AppConfigdef method_missing(name, *args, &block) if args.size == 1 @config[name] = args.first else @config[name] || nil end # if block_given? можем ообработать и блок, если он переданend
Теперь мы можем добавить в конфигурационный файл любой новый параметр, который не описан в классе AppConfig.
AppConfig.setup do ... database_type "postges"end
🛠 Текущие настройки: host: "127.0.0.1" port: 6380 logging: {:level=>:info} pool: {:size=>20} database_type: "postges"
Для корректной обработки методов «на лету», которые попадают в method_missing, необходимо переопределить метод respond_to_missing?. В таком случае, метод respond_to? будет возвращать true для всех методов, даже если они не объявлены явно.
# добавляем метод в AppConfigdef respond_to_missing?(name, include_private = false) trueend
Готово! Мы получили легкий и интуитивно-понятный синтаксис для конфигурации нашего приложения.
Заключение
Пример с AppConfig наглядно демонстрирует гибкие инструменты Ruby для написания кода близкого к естественному языку. Всем известный Ruby on Rails, как самый яркий пример использования DSL, позволяет разработчикам абстрагироваться от низкоуровневых деталей реализации и сосредоточиться на бизнес-логике.
В следующий раз, используя validates или belongs_to, вы будете точно знать, что стоит за этими строками.
ссылка на оригинал статьи https://habr.com/ru/articles/1023302/