Другое

Автозагрузка констант домена в Rails 8 при инициализации

Узнайте, как правильно автозагружать константы домена в Rails 8 во время инициализации без ручных require. Решения проблем с таймингом автозагрузки Zeitwerk для

Как правильно автоматически загружать константы домена в Rails 8 во время инициализации без ручных require?

Я создаю контейнер внедрения зависимостей в приложении Rails 8 в файле config/initializer/command_bus.rb. Проблема в том, что константы из слоя домена не загружаются автоматически во время инициализации, и мне приходится вручную требовать файлы:

ruby
require Rails.root.join("app/domain")
require Rails.root.join("app/domain/business")
require Rails.root.join("app/domain/business/events/business_created")
require Rails.root.join("app/domain/business/handlers/business_command_handler")
require Rails.root.join("app/domain/business/commands/create_business")
require Rails.root.join("app/domain/business/aggregate")

Я попытался добавить каталог app в autoload_paths в application.rb, но это не помогло. Что я упускаю в механизме автозагрузки Rails 8 во время инициализации?

Вот мой текущий код инициализатора:

ruby
App.register(:event_store, singleton: true) do
  Rails.configuration.event_store
end

App.register(:business_command_handler, singleton: true) do
  Domain::Business::Handlers::BusinessCommandHandler.new(App.resolve(:event_store))
end

App.register(:command_bus, singleton: true) do
  bus = CommandBus::Bus.new

  bus.use(CommandBus::Middleware::LoggingMiddleware.new)

  bus.register(
    Domain::Business::Commands::CreateBusiness,
    App.resolve(:business_command_handler)
  )

  bus
end

Константа Domain остаётся неопределённой, если я не требую файлы вручную. Как можно настроить Rails 8 так, чтобы эти константы домена автоматически загружались во время инициализации?

Rails 8 использует Zeitwerk в качестве системы автозагрузки по умолчанию, что приводит к изменению поведения во время инициализации по сравнению со старой системой автозагрузки. Основная проблема заключается в том, что во время инициализации Zeitwerk ещё не полностью настроен, поэтому константы домена, упомянутые в инициализаторах, остаются неопределёнными, если явно не загрузить их. Это фундаментальное изменение в Rails 8, поскольку инициализация выполняется до того, как система автозагрузки полностью функционирует.


Содержание


Понимание автозагрузки Rails 8 и Zeitwerk

Rails 8 переключился на Zeitwerk в качестве стандартного автозагрузчика, заменив старый механизм autoload. Zeitwerk работает, сканируя структуру каталогов вашего приложения и сопоставляя пути файлов с именами констант. Когда ссылка на константу, которая ещё не определена, Zeitwerk автоматически загружает соответствующий файл.

Ключевые особенности Zeitwerk:

  • Пакетная загрузка по умолчанию: Zeitwerk загружает все константы при запуске в продакшене
  • Явная привязка файлов: каждый файл должен следовать строгим правилам именования
  • Отсутствие глобального загрязнения пространства имён: константы загружаются только при обращении к ним
  • Время инициализации: автозагрузка недоступна во время фазы инициализации

Фаза инициализации в Rails происходит до того, как автозагрузчик полностью настроен. Это означает, что любые константы, упомянутые в инициализаторах, необходимо явно загрузить или изменить конфигурацию автозагрузчика.


Почему автозагрузка в фазе инициализации не работает

Во время инициализации приложение всё ещё находится в процессе настройки. Автозагрузчик ещё не активирован, поэтому константы, которые вы пытаетесь использовать, не существуют. Это преднамеренное поведение Rails 8, обеспечивающее предсказуемую и контролируемую последовательность запуска приложения.

Типичная последовательность:

  1. Загрузка конфигурации окружения
  2. Выполнение инициализаторов (ваш код)
  3. Активация автозагрузчика
  4. Завершение настройки приложения

Поскольку ваш инициализатор выполняется на шаге 2, а автозагрузка доступна только на шаге 3, константы, которые вы упоминаете, ещё не загружены.


Решения для констант домена во время инициализации

Решение 1: Использовать require с условной загрузкой

Самый прямой способ – продолжать использовать require, но сделать его более удобным:

ruby
# config/initializers/command_bus.rb

# Загрузить модули домена явно
Rails.root.join("app/domain").glob("**/*.rb").sort.each do |file|
  require file unless file.fnmatch?("*/application*") || file.fnmatch?("*/base*")
end

App.register(:event_store, singleton: true) do
  Rails.configuration.event_store
end

App.register(:business_command_handler, singleton: true) do
  Domain::Business::Handlers::BusinessCommandHandler.new(App.resolve(:event_store))
end

App.register(:command_bus, singleton: true) do
  bus = CommandBus::Bus.new
  bus.use(CommandBus::Middleware::LoggingMiddleware.new)
  
  bus.register(
    Domain::Business::Commands::CreateBusiness,
    App.resolve(:business_command_handler)
  )
  
  bus
end

Решение 2: Отложить инициализацию до после автозагрузки

Перенесите настройку внедрения зависимостей в более поздний хук инициализации:

ruby
# config/application.rb

module YourApp
  class Application < Rails::Application
    # ... существующая конфигурация ...
    
    config.after_initialize do
      # Выполняется после активации автозагрузчика
      setup_dependency_injection
    end
  end
end

# lib/dependency_setup.rb
module DependencySetup
  def self.setup
    App.register(:event_store, singleton: true) do
      Rails.configuration.event_store
    end

    App.register(:business_command_handler, singleton: true) do
      Domain::Business::Handlers::BusinessCommandHandler.new(App.resolve(:event_store))
    end

    App.register(:command_bus, singleton: true) do
      bus = CommandBus::Bus.new
      bus.use(CommandBus::Middleware::LoggingMiddleware.new)
      
      bus.register(
        Domain::Business::Commands::CreateBusiness,
        App.resolve(:business_command_handler)
      )
      
      bus
    end
  end
end

Решение 3: Настроить Zeitwerk на загрузку во время инициализации

Можно настроить Zeitwerk так, чтобы он загружал ваши модули домена во время инициализации:

ruby
# config/application.rb

module YourApp
  class Application < Rails::Application
    # ... существующая конфигурация ...
    
    config.autoloader = :classic if Rails.env.development?
  end
end

Это возвращает старую систему автозагрузки для разработки, что позволяет автозагрузку во время инициализации. Однако это не рекомендуется, так как возвращает проблемы, которые Zeitwerk был создан решить.

Решение 4: Использовать предварительную загрузку модуля

Создайте модуль, который загружает все константы вашего домена:

ruby
# app/domain.rb
module Domain
  def self.preload!
    Rails.root.join("app/domain").each_child do |dir|
      next if dir.file?
      
      dir.glob("**/*.rb").sort.each do |file|
        require_dependency file
      end
    end
  end
end

# config/initializers/command_bus.rb
Domain.preload!

# Остальной код инициализатора...

Альтернативные подходы к внедрению зависимостей

Решение 1: Использовать сервис‑объекты Rails

Вместо прямого обращения к константам домена в инициализаторах, используйте сервис‑объекты Rails:

ruby
# app/services/command_bus_service.rb
class CommandBusService
  def self.setup
    App.register(:event_store, singleton: true) do
      Rails.configuration.event_store
    end

    App.register(:business_command_handler, singleton: true) do
      BusinessCommandHandler.new(App.resolve(:event_store))
    end

    App.register(:command_bus, singleton: true) do
      bus = CommandBus::Bus.new
      bus.use(CommandBus::Middleware::LoggingMiddleware.new)
      
      bus.register(
        CreateBusinessCommand,
        App.resolve(:business_command_handler)
      )
      
      bus
    end
  end
end

# config/application.rb
Rails.application.config.after_initialize do
  CommandBusService.setup
end

Решение 2: Ленивое подключение в инициализаторах

Используйте ленивую оценку, чтобы отложить загрузку до момента, когда константы действительно нужны:

ruby
# config/initializers/command_bus.rb

App.register(:event_store, singleton: true) do
  Rails.configuration.event_store
end

App.register(:business_command_handler, singleton: true) do
  -> do
    require_dependency 'domain/business/handlers/business_command_handler'
    Domain::Business::Handlers::BusinessCommandHandler.new(App.resolve(:event_store))
  end.call
end

App.register(:command_bus, singleton: true) do
  bus = CommandBus::Bus.new
  bus.use(CommandBus::Middleware::LoggingMiddleware.new)
  
  # Ленивое подключение команды и обработчика
  command_class = -> do
    require_dependency 'domain/business/commands/create_business'
    Domain::Business::Commands::CreateBusiness
  end.call
  
  bus.register(
    command_class,
    App.resolve(:business_command_handler)
  )
  
  bus
end

Лучшие практики Domain‑Driven Design в Rails 8

1. Правильно организовать структуру домена

Убедитесь, что структура домена соответствует конвенциям Rails:

app/
  domain/
    business/
      commands/
        create_business.rb
      events/
        business_created.rb
      handlers/
        business_command_handler.rb
      aggregate.rb

Каждый файл должен определять свою константу последовательно:

ruby
# app/domain/business/commands/create_business.rb
module Domain
  module Business
    module Commands
      class CreateBusiness
        # реализация
      end
    end
  end
end

2. Использовать хуки запуска приложения

Используйте хуки Rails для правильного времени выполнения:

ruby
# config/application.rb
module YourApp
  class Application < Rails::Application
    config.before_initialize do
      # Загрузить основные модули домена
    end
    
    config.after_initialize do
      # Настроить внедрение зависимостей
      DependencyInjection.setup
    end
    
    config.to_prepare do
      # Финальная настройка домена
    end
  end
end

3. Учитывать новые возможности Rails 8

Rails 8 вводит несколько улучшений для лучшей организации приложения:

  • Используйте app/channels, app/jobs, app/mailers по конвенциям
  • Используйте улучшенную конфигурацию Zeitwerk
  • Воспользуйтесь новым каталогом app/services

4. Тестировать инициализацию

Убедитесь, что инициализация работает во всех окружениях:

ruby
# test/initialization_test.rb
class InitializationTest < ActiveSupport::TestCase
  test "константы домена доступны во время инициализации" do
    assert defined?(Domain::Business::Commands::CreateBusiness)
    assert defined?(Domain::Business::Handlers::BusinessCommandHandler)
  end
  
  test "контейнер внедрения зависимостей работает" do
    assert App.resolved?(:command_bus)
    assert App.resolved?(:event_store)
  end
end

Источники

  1. Руководство по инициализации Rails 8
  2. Документация Zeitwerk
  3. Руководство по автозагрузке и перезагрузке констант Rails
  4. Примечания к выпуску Rails 8

Заключение

Правильная автозагрузка констант домена в Rails 8 во время инициализации требует понимания различий во времени между фазой инициализации и активацией автозагрузчика. Ключевые решения включают:

  1. Явная загрузка: используйте require или require_dependency для констант домена в инициализаторах
  2. Отложенная инициализация: перенесите настройку внедрения зависимостей в config.after_initialize
  3. Ленивая загрузка: отложите подключение констант до момента их фактического использования
  4. Сервис‑объекты: используйте сервис‑объекты Rails как посредник между инициализаторами и константами домена

Самый рекомендуемый подход – отложенная инициализация с config.after_initialize или создание сервис‑объекта, который обрабатывает настройку внедрения зависимостей после активации автозагрузчика. Это сохраняет современное поведение автозагрузки Rails 8 и решает проблему временных ограничений. Помните, что Zeitwerk создан для предсказуемости и производительности, поэтому работа с его временными ограничениями, а не борьба с ними, приведёт к более поддерживаемому коду.

Авторы
Проверено модерацией
Модерация