Автозагрузка и перезагрузка констант

Это руководство документирует, как работает автозагрузка и перезагрузка констант.

После его прочтения, вы узнаете:

  • Ключевые аспекты констант в Ruby
  • Что такое autoload_paths
  • Как работает автозагрузка констант
  • Что такое require_dependency
  • Как работает перезагрузка констант
  • Решения для распространенных проблем автозагрузки

1. Введение

Ruby on Rails позволяет приложениям быть написанными так, как будто их код был предварительно загружен.

В обычных классах в программах на Ruby необходимо загружать их зависимости:

require 'application_controller'
require 'post'

class PostsController < ApplicationController
  def index
    @posts = Post.all
  end
end

Наши рубишные инстинкты быстро усматривают некоторую избыточность тут: если классы были объявлены в файлах, соответствующих их имени, нельзя ли как-то автоматизировать их загрузку? Можно было бы избавиться от просмотра файла на предмет сломавшихся зависимостей.

Более того, Kernel#require загружает файлы единожды, но разработка становится гораздо приятнее, когда для перезагрузки кода не нужно перезагружать сервер. Было бы неплохо иметь возможность использования Kernel#load в development и Kernel#require в production.

Разумеется, эти особенности представлены Ruby on Rails, в котором мы просто пишем

class PostsController < ApplicationController
  def index
    @posts = Post.all
  end
end

Настоящее руководство раскрывает, как это работает.

2. Сведения о константах

Хотя константы просты в большинстве языков программирования, они являются большой темой в Ruby.

Документирование констант Ruby находится за пределами этого руководства, но тем не менее мы собираемся осветить несколько тем. Понимание следующих разделов играет важную роль в понимании автозагрузки и перезагрузки констант.

2.1. Вложенность

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

module XML
  class SAXParser
    # (1)
  end
end

Вложенность в любое заданное место — это коллекция из внешних вложенных объектов класса и модуля для доступа снаружи. Вложенность в любом заданном месте можно просмотреть с помощью Module.nesting. Например, в предыдущем примере вложенностью (1) является

[XML::SAXParser, XML]

Важно понимать, что вложенность состоит из объектов класса и модуля, она не делает ничего с константами, использованными для доступа к ним, и никак не относится к их именам.

Например, хотя это определение похоже на предыдущее:

class XML::SAXParser
  # (2)
end

вложенность (2) отличается:

[XML::SAXParser]

XML не принадлежит ей.

Из этого примера мы видим, что имя класса или модуля, принадлежащего определенной вложенности, не обязательно соотносится с пространством имен в этом месте.

Более того, они абсолютно независимы, для примера

module X
  module Y
  end
end

module A
  module B
  end
end

module X::Y
  module A::B
    # (3)
  end
end

Вложенность (3) состоит из двух объектов модуля:

[A::B, X::Y]

Таким образом, она не только не заканчивается на A, который даже не принадлежит вложенности, она также содержит X::Y, который независим от A::B.

Вложенность — это внутренний стек, поддерживаемый интерпретатором, и он изменяется в соответствии со следующими правилами:

  • Объект класса, следующий за ключевым словом class добавляется, когда выполняется его тело, а затем извлекается.

  • Объект класса, следующий за ключевым словом module добавляется, когда выполняется его тело, а затем извлекается.

  • Синглтон-класс, открытый с помощью class << object добавляется, затем извлекается.

  • Когда вызывается instance_eval со строковым аргументом, синглтон-класс получателя добавляется во вложенность вычисляемого кода. Когда вызываются class_eval или module_eval со строковым аргументом, получатель добавляется во вложенность вычисляемого кода.

  • Вложенность в коде верхнего уровня, интерпретируемого Kernel#load, пустая, за исключением случая, когда вызов load получает true в качестве второго аргумента, в случае чего Ruby добавляет вновь созданный анонимный модуль.

Любопытно, что блоки не изменяют стек. В частности, блоки, переданные в Class.new и Module.new, не добавляют определяемые класс или модуль в их вложенность. Это одно из отличий между определением классов или модулей тем или иным способом.

2.2. Определения класса и модуля — это назначения констант

Допустим, следующий код создает класс (а не переоткрывает его):

class C
end

Ruby создает константу C в Object и сохраняет в эту константу объект класса. Именем экземпляра класса является "C", строка, названная по имени константы.

То есть,

class Project < ApplicationRecord
end

выполняет назначение константе, эквивалентное

Project = Class.new(ApplicationRecord)

включая установление имени класса в качестве побочного эффекта:

Project.name # => "Project"

У назначения констант есть специальное правило, благодаря которому это происходит: если назначаемый объект является анонимным классом или модулем, Ruby устанавливает имя объекта по константе.

С этого момента то, что будет происходить с константой или экземпляром, не имеет значения. Например, константа может быть удалена, объект класса может быть назначен другой константе, больше не храниться в константе, и так далее. Как только имя установлено, оно не меняется.

Аналогично, создание модуля с помощью ключевого слова module, как в

module Admin
end

выполняет назначение константы, эквивалентное

Admin = Module.new

включая установление имени в качестве побочного эффекта:

Admin.name # => "Admin"

Контекст выполнения блока, переданного в Class.new или Module.new, не полностью эквивалентен контексту тела определений с помощью ключевых слов class и module. Но обе идиомы приводят к одинаковому назначению константы.

Таким образом, когда кто-то говорит "класс String", в реальности это означает: объект класса, хранимого в константе с именем "String" в объекте класса, хранимого в константе Object. С другой стороны, String — это обычная константа Ruby, и к ней применяется все, относящееся к константам, например применяемые к ней алгоритмы резолюции.

Более того, в контроллере

class PostsController < ApplicationController
  def index
    @posts = Post.all
  end
end

Post — это не синтаксис для класса. Скорее, Post — это обычная константа Ruby. Если все нормально, константа вычисляет объект, отвечающий на all.

Вот почему мы говорим об автозагрузке констант, в Rails есть возможность загрузки констант на лету.

2.3. Константы хранятся в модулях

Константы принадлежат модулям в буквальном смысле. У классов и модулей есть таблица констант; рассматривайте ее, как хэш-таблицу.

Давайте проанализируем пример, чтобы в действительности понять, что это означает. Хотя распространенные упрощения языка, наподобие "класс String", удобны, в обучающих целях изложение здесь будет более точным.

Рассмотрим следующее определение модуля:

module Colors
  RED = '0xff0000'
end

Во-первых, когда обрабатывается ключевое слово module, интерпретатор создает новую запись в таблице констант объекта класса, хранимого в константе Object. Упомянутая запись связывает имя "Colors" с вновь созданным объектом модуля. Далее интерпретатор устанавливает имя нового объекта модуля равным строке "Colors".

Далее, когда интерпретируется тело определения модуля, создается запись в таблице констант объекта модуля, хранящегося в константе Colors. Эта запись связывает имя "RED" со строкой "0xff0000".

В частности, Colors::RED никоим образом не относится к любой другой константе RED, которая может находиться в любом другом объекте класса или модуля. Если такие существуют, они будут иметь другие записи в их соответствующих таблицах констант.

Обратите особенное внимание в предыдущих абзацах на различие между объектами класса и модуля, именами констант и объектами значений, связанными с ними в таблицах констант.

2.4. Алгоритмы резолюции

2.4.1. Алгоритмы резолюции для относительных констант

Давайте определим, что в любом месте кода, cref будет обозначать первый элемент вложенности, если она не пустая, а в противном случае Object.

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

  • Если вложенность не пустая, константа ищется в ее элементах по порядку. Предки этих элементов игнорируются.

  • Если не найдена, алгоритм проходит по цепочке предков у cref.

  • Если не найдена и cref — это модуль, константа ищется в Object.

  • Если не найдена, вызывается const_missing на cref. Реализация по умолчанию для const_missing вызывает NameError, но может быть переопределена.

Автозагрузка Rails не эмулирует этот алгоритм, но его отправной точкой является имя константы, которую нужно автоматически загрузить, и cref. Подробнее в главе Относительные ссылки.

2.4.2. Алгоритмы резолюции для ограниченных констант

Ограниченные константы выглядят так:

Billing::Invoice

Billing::Invoice состоит из двух констант: Billing является относительной, и она разрешается с помощью алгоритма из предыдущего раздела.

Начинание с двоеточий делает первый сегмент абсолютным, а не относительным: ::Billing::Invoice. Это принуждает искать Billing среди констант верхнего уровня.

Invoice, с другой стороны, ограничена Billing, и далее нам нужно разрешить ее. Давайте определим, что parent будет обозначать этот ограничивающий объект класса или модуля, то есть Billing в вышеприведенном примере. Алгоритм для ограниченных констант выполняется так:

  • Константа ищется в parent и его предках. В Ruby >= 2.5, Object пропускается, если присутствует среди предков. Kernel и BasicObject все еще проверяются.

  • Если поиск неудачный, в parent вызывается const_missing. Реализация по умолчанию для const_missing вызывает NameError, но может быть переопределена.

В Ruby < 2.5 String::Hash вычисляется как Hash и интерпретатор выдает предупреждение: "toplevel constant Hash referenced by String::Hash". Начиная с 2.5, String::Hash вызывает NameError, потому что Object пропускается.

Как видите, этот алгоритм проще, чем для относительных констант. В частности, вложенность не имеет значения, и нет специального случая для модулей, когда у них или их предков нет константы, Object не проверяется.

Автозагрузка Rails не эмулирует этот алгоритм, но его отправной точкой является имя константы, которую нужно автоматически загрузить, и parent. Подробнее в главе Ограниченные ссылки.

3. Словарь

3.1. Родительские пространства имен

Для заданной строки с константой мы определяем родительское пространство имен, как строку, полученную в результате удаления самого правого сегмента.

Например, родительским пространством имен для строки "A::B::C" является строка "A::B", родительским пространством имен для "A::B" является "A", и родительским пространством имен для "A" является "".

Впрочем, интерпретация родительского пространства имен может быть запутанной, если размышлять о классах и модулях. Давайте рассмотрим модуль M с именем "A::B":

  • Родительское пространство имен, "A", может не отражать вложенность в заданной точке.

  • Константа A может больше не существовать, некий код мог удалить ее из Object.

  • Если A существует, класс или модуль, который изначально был в A, может больше не быть там. Например, если после того, как константу удалили, произошло новое назначение константы, положившее иной объект в нее.

  • В этом случае может произойти так, что переназначенная A содержит новый класс или модуль, также названный "A"!

  • В прошлых сценариях M больше не будет доступен с помощью A::B. но сам объект модуля будет где-то существовать, и его имя по прежнему будет "A::B".

Идея родительского пространства имен является сердцевиной алгоритмов автозагрузки и помогает объяснить и интуитивно понять их мотивацию, но, как вы увидели, эта метафора легко ускользает. С учетом крайних случаев, о которых мы только что говорили, всегда принимайте во внимание то, что под "родительским пространством имен" руководство понимает исключительно определенное образование строк.

3.2. Механизм загрузки

Rails автоматически загружает файлы с помощью Kernel#load, когда config.cache_classes является false, по умолчанию в среде development, а в противном случае с помощью Kernel#require, по умолчанию в среде production.

Kernel#load позволяет Rails выполнять файлы более, чем однажды, если включена перезагрузка констант.

Это руководство часто использует слово "загрузка", что означает, что интерпретируется данный файл, но фактическим механизмом может быть Kernel#load или Kernel#require, в зависимости от этого флажка.

4. Доступность автозагрузки

Rails всегда способен автоматически загружать свое окружение на месте. Например, команда runner делает автозагрузку:

$ bin/rails runner 'p User.column_names'
["id", "email", "created_at", "updated_at"]

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

По умолчанию Rails нетерпеливо загружает файлы приложения, когда он запускается в режиме production, поэтому большая часть автозагрузки, происходящая в development, не происходит. Но автозагрузка все еще может произойти в течение нетерпеливой загрузки.

Например, дан

class BeachHouse < House
end

если House все еще неизвестен, когда нетерпеливо загружается app/models/beach_house.rb, Rails его загрузит автоматически.

5. autoload_paths

Как вам, наверное, известно, когда require получает относительное имя файла:

require 'erb'

Ruby ищет файл в директориях, перечисленных в $LOAD_PATH. То есть, Ruby перебирает все свои директории и для каждой из них проверяет, имеется ли в ней файл с именем "erb.rb", или "erb.so", или "erb.o", или "erb.dll". Если он находит один из них, то интерпретатор загружает его и заканчивает поиск. В противном случае он снова пытается сделать то же самое для следующей директории в списке. Если список заканчивается, вызывается LoadError.

Мы попытаемся подробнее раскрыть, как работает автозагрузка констант, позже, но идея в том, что когда вызвана, но отсутствует константа, наподобие Post, и, к примеру, имеется файл post.rb в app/models, Rails собирается найти его, вычислить его и получить определение Post в качестве побочного эффекта.

Отлично, в Rails есть коллекция директорий, подобная $LOAD_PATH, для поиска post.rb. Эта коллекция называется autoload_paths, и, по умолчанию, она содержит:

  • Все поддиректории app в приложении и engine-ах, существующие на момент загрузки. Например, app/controllers. Нет каких-либо значений по умолчанию, любая произвольная директория, наподобие app/workers, будет автоматически принадлежать autoload_paths.

  • Любые существующие директории второго уровня с именем app/*/concerns в приложении и engine-ах.

  • Директория test/mailers/previews.

А также эта коллекция настраивается с помощью config.autoload_paths. Например, lib была в списке пару лет назад, но сейчас уже нет. Можно ее добавить, добавив в config/application.rb:

config.autoload_paths << "#{Rails.root}/lib"

config.autoload_paths недоступна для изменения из конфигурационных файлов, относящихся к окружению.

Значение autoload_paths можно просмотреть. В только что сгенерированном приложении она (отредактировано):

$ bin/rails r 'puts ActiveSupport::Dependencies.autoload_paths'
.../app/assets
.../app/channels
.../app/controllers
.../app/controllers/concerns
.../app/helpers
.../app/jobs
.../app/mailers
.../app/models
.../app/models/concerns
.../activestorage/app/assets
.../activestorage/app/controllers
.../activestorage/app/javascript
.../activestorage/app/jobs
.../activestorage/app/models
.../actioncable/app/assets
.../actionview/app/assets
.../test/mailers/previews

autoload_paths вычисляется и кэшируется на этапе процесса инициализации. Необходимо перезапустить приложение, чтобы отразить изменения в структуре директорий.

6. Алгоритмы автозагрузки

6.1. Относительные ссылки

Это доступно только для Ruby < 2.5.

Относительная константная ссылка может появиться в нескольких местах, например, в

class PostsController < ApplicationController
  def index
    @posts = Post.all
  end
end

все три константные ссылки относительны.

6.1.1. Константы после ключевых слов class и module

Ruby выполняет поиск констант, следующих за ключевыми словами class или module, так как ему необходимо знать, собирается ли класс или модуль быть созданным или переоткрытым.

Если константа в этом месте не определена, она не рассматривается отсутствующей константой, автозагрузка не срабатывает.

Таким образом, в предыдущем примере, если PostsController не определен на момент интерпретации файла, автозагрузка Rails не сработает, Ruby всего лишь определит контроллер.

6.1.2. Константы верхнего уровня

С другой стороны, если ApplicationController неизвестен, константа рассматривается отсутствующей, и Rails собирается предпринять автозагрузку.

Чтобы загрузить ApplicationController, Rails проходит по autoload_paths. Сначала он проверяет, существует ли app/assets/application_controller.rb. Если нет, что неудивительно, он продолжает и находит app/controllers/application_controller.rb.

Если файл определяет константу ApplicationController, то все хорошо, в противном случае вызывается LoadError:

unable to autoload constant ApplicationController, expected
<full path to application_controller.rb> to define it (LoadError)

Rails не требует, чтобы значения автозагружаемых констант были объектом класса или модуля. Например, если файл app/models/max_clients.rb определяет MAX_CLIENTS = 100, автозагрузка MAX_CLIENTS также сработает.

6.1.3. Пространства имен

Автозагрузка ApplicationController ищет непосредственно в директориях autoload_paths, так как вложенность в этом месте пустая. Ситуация с Post иная, вложенность в этой строчке является [PostsController], и в игру вступает поддержка пространств имен.

Основная идея в том, что для

module Admin
  class BaseController < ApplicationController
    @@all_roles = Role.all
  end
end

чтобы автоматически загрузить Role, мы собираемся по очереди проверить, определен ли он в текущем или родительском пространстве имен. Таким образом, концептуально мы хотим попытаться автоматически загрузить любой из

Admin::BaseController::Role
Admin::Role
Role

в этом порядке. Такова идея. Для этого Rails ищет в autoload_paths следующие имена файлов в указанном порядке:

admin/base_controller/role.rb
admin/role.rb
role.rb

производя дополнительные поиски модулей в директориях, о которых мы скоро расскажем.

'Constant::Name'.underscore дает относительный путь без расширения для имени файла, в котором ожидается определение для Constant::Name.

Давайте посмотрим, как Rails автоматически загружает константу Post в вышеприведенном PostsController, предполагая, что модель Post определена в app/models/post.rb.

Сначала он проверяет posts_controller/post.rb в autoload_paths:

app/assets/posts_controller/post.rb
app/controllers/posts_controller/post.rb
app/helpers/posts_controller/post.rb
...
test/mailers/previews/posts_controller/post.rb

Так как поиск завершился неуспешно, выполняется схожий поиск для директорий, почему — мы узнаем в следующем разделе:

app/assets/posts_controller/post
app/controllers/posts_controller/post
app/helpers/posts_controller/post
...
test/mailers/previews/posts_controller/post

Если все эти попытки провалятся, то Rails снова начнет поиск в родительском пространстве имен. В этом случае остается только верхний уровень:

app/assets/post.rb
app/controllers/post.rb
app/helpers/post.rb
app/mailers/post.rb
app/models/post.rb

Соответствующий файл найдется в app/models/post.rb. Тут поиск остановится и файл загрузится. Если файл в действительности определяет Post, все в порядке, в противном случае будет вызвано LoadError.

6.2. Ограниченные ссылки

Когда отсутствует ограниченная константа, Rails не ищет ее в родительских пространствах имен. Но есть нюанс: когда отсутствует константа, Rails неспособен определить, была ли она относительной или ограниченной.

Например, рассмотрим

module Admin
  User
end

и

Admin::User

Если отсутствует User, Rails в любом случае знает, что константа с именем "User" отсутствует в модуле с именем "Admin".

Если на верхнем уровне имеется User, Ruby вернет его для первого примера, но не для второго. В целом, Rails не эмулирует алгоритмы разрешения констант Ruby, но в этом случае он пытается, используя следующую эвристику:

Если ни в одном из родительских пространств имен класса или модуля нет отсутствующей константы, тогда Rails полагает, что ссылка относительная. В противном случае ограниченная.

Например, если этот код запускает автозагрузку

Admin::User

и константа User уже присутствует в Object, тогда невозможна такая ситуация

module Admin
  User
end

так как в этом случае Ruby вычислил бы User, и автозагрузка в первом случае не была бы запущена. Поэтому Rails полагает ограниченную ссылку и рассматривает файл admin/user.rb и директорию admin/user в качестве единственных правильных вариантов.

На практике это работает достаточно хорошо, пока вложенность соответствует всем родительским пространствам имен, и константы, позволяющие применить правило, известны к этому времени.

Однако, автозагрузка происходит по требованию. Есть шанс, что User верхнего уровня еще не был загружен, тогда Rails предполагает по соглашению относительную ссылку.

Конфликты имен подобного рода редки на практике, но если происходят, решением является require_dependency, чтобы убедиться, что константа, необходимая для запуска эвристики, определена в конфликтующем месте.

6.3. Автоматические модули

Когда модуль выступает в роли пространства имен, Rails не требует, чтобы приложение определяло для него файл, достаточно соответствующей директории.

Допустим, у приложения есть админка, контроллеры которой хранятся в app/controllers/admin. Если модуль Admin пока не загружен на момент обращения к Admin::UsersController, Rails необходимо сперва автоматически загрузить константу Admin.

Если в autoload_paths имеется файл с именем admin.rb, Rails загрузило бы его, но если такого файла нет, а найдена директория с именем admin, Rails на лету создаст пустой модуль и назначит его константе Admin.

6.4. Общая процедура

Относительные ссылки считаются отсутствующими в cref места, где они вызваны, а ограниченные ссылки считаются отсутствующими в их parent (определение cref смотрите в Алгоритмы резолюции для относительных констант в начале этого руководства, а определение parent — в Алгоритмы резолюции для ограниченных констант)

Процедура автозагрузки константы C в произвольной ситуации следующая:

if (класс или модуль, в котором отсутствует C, это Object)
  ns = ''
else
  M = (класс или модуль, в котором отсутствует C)

  if (M анонимный)
    ns = ''
  else
    ns = M.name
  end
end

loop do
  # Ищем обычный файл.
  for dir in autoload_paths
    if (существует файл "#{dir}/#{ns.underscore}/c.rb")
      load/require "#{dir}/#{ns.underscore}/c.rb"

      if (теперь определена C)
        return
      else
        raise LoadError
      end
    end
  end

  # Ищем автоматический модуль.
  for dir in autoload_paths
    if (существует директория "#{dir}/#{ns.underscore}/c")
      if (ns пустая строка)
        C = (Module.new в Object) and return
      else
        C = (Module.new в ns.constantize) and return
      end
    end
  end

  if (ns пустое)
    # Мы достигли верхнего уровня и не нашли константу.
    raise NameError
  else
    if (C существует в любом из родительских пространств имен)
      # Эвристика для ограниченных констант.
      raise NameError
    else
      # Снова пытаемся в родительском пространстве имен.
      ns = (родительское пространство имен для ns) and retry
    end
  end
end

7. require_dependency

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

Однако бывает, когда вы хотите убедиться, что определенная константа известна к моменту, когда выполнение достигнет некоторого кода. require_dependency представляет способ загрузки файла с помощью текущего механизма загрузки и отслеживания констант, определенном в этом файле так, как будто они были автоматически загружены, чтобы при надобности перезагрузить их.

require_dependency редко необходим, но имеется ряд сценариев, таких как Автозагрузка и STI и Когда константы не находятся.

В отличие от автозагрузки, require_dependency не ожидает, что файл определяет какую-либо определенную константу. Чтобы использование этой особенности не являлось плохой практикой, файлы и константы должны соответствовать друг другу.

8. Перезагрузка констант

Когда config.cache_classes равно false, Rails способен перезагружать автозагруженные константы.

Например, если вы в консоли и отредактировали какой-то файл, код может быть перезагружен с помощью команды reload!:

> reload!

Когда запущено приложение, код перезагружается когда изменяется что-то, относящееся к его логике. Для этого Rails мониторит ряд вещей:

  • config/routes.rb.

  • Локали.

  • Файлы Ruby в autoload_paths.

  • db/schema.rb и db/structure.sql.

Если что-то из них изменяется, имеется промежуточная программа, определяющая это и перезагружающая код.

Автозагрузка отслеживает автозагруженные константы. Перезагрузка реализована как удаление их из соответствующих классов и модулей с помощью Module#remove_const. Таким образом, когда начнет исполняться код, эти константы снова становятся неизвестными, и файлы загружаются по требованию.

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

9. Module#autoload не задействован

Module#autoload представляет ленивый способ загрузки констант, который полностью интегрирован с алгоритмами поиска констант в Ruby, API динамических констант, и так далее. Он весьма прозрачный.

Rails широко используют его внутренне, чтобы отложить столько работы, сколько возможно в процессе загрузки. Но автозагрузка констант в Rails не реализуется с помощью Module#autoload.

Одной из возможных реализаций, основанных на Module#autoload, могла бы быть прохождение по дереву приложения и вызов autoload, связывающий имена существующих файлов с именами констант по соглашению.

Имеется ряд причин, предотвращающих от использования этой реализации в Rails.

Например, Module#autoload способен только загружать файлы с помощью require, поэтому перезагрузка невозможна. Кроме того, он использует собственный require, который не Kernel#require.

Далее, он не представляет способ убрать определение в случае удаления файла. Если константа была удалена с помощью Module#remove_const, ее autoload снова не включится. А также он не поддерживает ограниченные имена, поэтому файлы в пространстве имен должны интерпретироваться в процессе прохождения по дереву, чтобы установить собственные вызовы autoload, но эти файлы могут иметь собственные константные ссылки, которые еще не настроены.

Реализация с помощью Module#autoload могла бы быть великолепной, но, как видите, как минимум сегодня она не возможна. Автозагрузка констант в Rails реализована с помощью Module#const_missing, и поэтому у нее есть свои соглашения, документированные в этом руководстве.

10. Распространенные случаи

10.1. Вложенность и ограниченные константы

Давайте рассмотрим

module Admin
  class UsersController < ApplicationController
    def index
      @users = User.all
    end
  end
end

и

class Admin::UsersController < ApplicationController
  def index
    @users = User.all
  end
end

Чтобы разрешить User, Ruby проверяет Admin в первом, но не в последнем случае, так как он не принадлежит вложенности (подробнее во Вложенность и Алгоритмы резолюции)

К сожалению, автозагрузка Rails не знает о вложенности в месте, где отсутствует константа, и не способен сработать так, как Ruby. В частности, Admin::User автоматически загрузится в любом случае.

Хотя ограниченные константы с ключевыми словами class и module технически могут работать с автозагрузкой в некоторых случаях, вместо них предпочтительней использовать относительные константы:

module Admin
  class UsersController < ApplicationController
    def index
      @users = User.all
    end
  end
end

10.2. Автозагрузка и STI

Наследование с единой таблицей (STI) — это особенность Active Record, позволяющая хранить иерархию моделей в одной отдельной таблице. API таких моделей знает об иерархии и инкапсулирует некоторые общие потребности. Например, имеем следующие классы:

# app/models/polygon.rb
class Polygon < ApplicationRecord
end

# app/models/triangle.rb
class Triangle < Polygon
end

# app/models/rectangle.rb
class Rectangle < Polygon
end

Triangle.create создает запись, представляющую треугольник, и Rectangle.create создает запись, представляющую прямоугольник. Если id — это идентификатор существующей записи, Polygon.find(id) возвратит объект правильного типа.

Методы, оперирующие с коллекциями, также в курсе об иерархии. Например, Polygon.all возвратит все записи из таблицы, так как все прямоугольники и треугольники — многоугольники. Active Record берет на себя заботу о возврате экземпляров соответствующего класса в результирующей выборке.

Типы автоматически загружаются при необходимости. Например, если Polygon.first — это прямоугольник, и Rectangle не был еще загружен, Active Record автоматически загрузит его, и запись инициализируется корректно.

Все хорошо, но если вместо выполнения запросов, основанных на корневом классе, нам необходимо работать на некотором подклассе, становится интереснее.

Хотя, работая с Polygon, вам не нужно беспокоится обо всех его потомках, так как все в таблице многоугольники по определению, но при работе с подклассами Active Record необходимо перечислить типы, которые он ищет. Давайте рассмотрим пример.

Rectangle.all загружает только прямоугольники, добавляя ограничение типа в запрос:

SELECT "polygons".* FROM "polygons"
WHERE "polygons"."type" IN ("Rectangle")

Теперь давайте представим подкласс Rectangle:

# app/models/square.rb
class Square < Rectangle
end

Теперь Rectangle.all должен вернуть прямоугольники и квадраты:

SELECT "polygons".* FROM "polygons"
WHERE "polygons"."type" IN ("Rectangle", "Square")

Но тут есть нюанс: как Active Record узнает, что класс Square вообще существует?

Даже если существует файл app/models/square.rb, определяющий класс Square, если код еще не использовал этот класс, Rectangle.all выполнит запрос

SELECT "polygons".* FROM "polygons"
WHERE "polygons"."type" IN ("Rectangle")

Это не ошибка, запрос включает всех известных потомков Rectangle.

Способом, гарантирующим что это будет работать правильно, независимо от порядка выполнения, является загрузка вручную прямых подклассов внизу файла, определяющего каждый промежуточный класс:

# app/models/rectangle.rb
class Rectangle < Polygon
end
require_dependency 'square'

Это должно произойти для каждого промежуточного (не корневого и не листового) класса. Корневой класс не охватывает запрос по типу и, следовательно, не обязательно должен знать всех своих потомки.

10.3. Автозагрузка и require

Для файлов, определяющих константы, которые необходимо автоматически загрузить, нельзя делать require:

require 'user' # НЕ НАДО ТАК

class UsersController < ApplicationController
  ...
end

Тут есть две ловушки в режиме development:

  • Если User автоматически загружен до выполнения require, app/models/user.rb запустится снова, так как load не обновляет $LOADED_FEATURES.

  • Если require выполнится сначала, Rails не пометит User как автозагруженную константу, и изменения в app/models/user.rb не будут перезагружены.

Просто используйте автозагрузку всегда, никогда не смешивайте автозагрузку и require. В крайнем случае, если некоторому файлу абсолютно необходимо загрузить определенный файл, используйте require_dependency, который прекрасно работает с автозагрузкой констант. Хотя это опция крайне редко необходима на практике.

Конечно, использование require в автоматически загруженных файлах для загрузки обычных сторонних библиотек — это прекрасно, и Rails способен отличить их константы, так как они не помечены, как автозагруженные.

10.4. Автозагрузка и инициализаторы

Рассмотрим это присваивание в config/initializers/set_auth_service.rb:

AUTH_SERVICE = if Rails.env.production?
  RealAuthService
else
  MockedAuthService
end

Целью этой настройки может быть то, что приложение использует класс, соответствующий окружению, с помощью AUTH_SERVICE. В режиме development автоматически загружается MockedAuthService при запуске инициализатора. Допустим, мы сделали пару запросов, изменили его реализацию, и снова обратились к приложению. К нашему удивлению, изменения не отразились. Почему?

Как мы видели раньше, Rails удаляет автозагруженные константы, но AUTH_SERVICE хранит оригинальный объект класса. Устаревший, недоступный с помощью оригинальной константы, но функционирующий.

Следующий пример обобщает ситуацию:

class C
  def quack
    'quack!'
  end
end

X = C
Object.instance_eval { remove_const(:C) }
X.new.quack # => quack!
X.name      # => C
C           # => uninitialized constant C (NameError)

В связи с этим, автоматически загружать константы при инициализации приложения является плохой идеей.

В вышеприведенном случае можно реализовать динамическую точку доступа:

# app/models/auth_service.rb
class AuthService
  if Rails.env.production?
    def self.instance
      RealAuthService
    end
  else
    def self.instance
      MockedAuthService
    end
  end
end

и использовать в приложении AuthService.instance. AuthService будет автоматически загружен по требованию и будет дружить с автозагрузкой.

10.5. require_dependency и инициализаторы

Как мы уже видели, require_dependency загружает файлы дружественным с автозагрузкой способом. Впрочем, обычно такой вызов не имеет смысла в инициализаторе.

Кто-то может додуматься сделать вызовы require_dependency в инициализаторе, чтобы убедиться, что некоторые константы загружены первоначально, например, как попытку решить проблему со STI.

Проблема в том, что в режиме development автозагруженные константы удаляются, если было какое-либо относящееся изменение в файловой системе. Если такое случается, мы находимся в той же самой ситуации с инициализатором, которую хотим избежать!

Вызовы require_dependency стратегически должны быть написаны в местах автозагрузки.

10.6. Когда константы не находятся

10.6.1. Относительные ссылки

Давайте рассмотрим летный симулятор. В приложении есть модель полета по умолчанию

# app/models/flight_model.rb
class FlightModel
end

которая может быть переопределена для каждого самолета, например

# app/models/bell_x1/flight_model.rb
module BellX1
  class FlightModel < FlightModel
  end
end

# app/models/bell_x1/aircraft.rb
module BellX1
  class Aircraft
    def initialize
      @flight_model = FlightModel.new
    end
  end
end

Инициализатор хочет создать BellX1::FlightModel и во вложенности есть BellX1, что выглядит хорошо. Но, если модель полета по умолчанию загружена, а модель для Bell-X1 нет, интерпретатор способен разрешить FlightModel верхнего уровня, и автозагрузка для BellX1::FlightModel не сработает.

Этот код зависит от последовательности выполнения.

Подобный тип двусмысленности часто можно разрешить с помощью ограниченных констант:

module BellX1
  class Plane
    def flight_model
      @flight_model ||= BellX1::FlightModel.new
    end
  end
end

А также, решением является require_dependency:

require_dependency 'bell_x1/flight_model'

module BellX1
  class Plane
    def flight_model
      @flight_model ||= FlightModel.new
    end
  end
end

10.6.2. Ограниченные ссылки

Даны

# app/models/hotel.rb
class Hotel
end

# app/models/image.rb
class Image
end

# app/models/hotel/image.rb
class Hotel
  class Image < Image
  end
end

Выражение Hotel::Image двусмысленное, так как оно зависит от последовательности выполнения.

Как мы видели раньше, Ruby ищет константу в Hotel и его предках. Если app/models/image.rb был загружен, но app/models/hotel/image.rb не был, Ruby не найдет Image в Hotel, но найдет в Object:

$ bin/rails r 'Image; p Hotel::Image' 2>/dev/null
Image # НЕ Hotel::Image!

Коду, вычисляющему Hotel::Image, нужно убедиться, что app/models/hotel/image.rb был загружен, возможно, с помощью require_dependency.

Хотя в таких случаях интерпретатор вызывает предупреждение:

warning: toplevel constant Image referenced by Hotel::Image

Эту удивляющую резолюцию констант можно увидеть на любом ограниченном классе:

2.1.5 :001 > String::Array
(irb):1: warning: toplevel constant Array referenced by String::Array
 => Array

Чтобы эта проблема проявилась, нужно чтобы ограниченное пространство имен было классом, так как Object не предок модулей.

10.7. Автозагрузка в синглтон-классе

Допустим, у нас имеются такие определения класса:

# app/models/hotel/services.rb
module Hotel
  class Services
  end
end

# app/models/hotel/geo_location.rb
module Hotel
  class GeoLocation
    class << self
      Services
    end
  end
end

Если Hotel::Services известен во время загрузки app/models/hotel/geo_location.rb, Services разрешится Ruby, так как Hotel принадлежит вложенности, когда открыт синглтон-класс Hotel::GeoLocation.

Но, если Hotel::Services неизвестен, Rails не сможет автоматически загрузить его, приложение вызовет NameError.

Причина в том, что автозагрузка включается для синглтон-класса, который анонимный, и, как мы уже видели, Rails проверяет только пространство имен верхнего уровня в этом крайнем случае.

Простым решением этой проблемы является ограничение константы:

module Hotel
  class GeoLocation
    class << self
      Hotel::Services
    end
  end
end

10.8. Автозагрузка в BasicObject

У прямых потомков BasicObject нет Object среди предков, и они не могут разрешить константы верхнего уровня:

class C < BasicObject
  String # NameError: uninitialized constant C::String
end

Когда вызывается автозагрузка, она работает необычно. Давайте рассмотрим:

class C < BasicObject
  def user
    User # НЕВЕРНО
  end
end

Так как Rails проверяет пространство имен верхнего уровня, User автоматически загрузится только в первый раз, когда вызовется метод user. Исключение получится только когда константа User будет известна в этом месте, в частности, при втором вызове user:

c = C.new
c.user # к удивлению произойдет, User
c.user # NameError: uninitialized constant C::User

так как он обнаруживает, что родительское пространство имен уже имеет константу (смотрите Ограниченные ссылки.)

Как и в чистом Ruby, в теле прямых потомков BasicObject всегда используйте абсолютные пути констант:

class C < BasicObject
  ::String # ПРАВИЛЬНО

  def user
    ::User # ПРАВИЛЬНО
  end
end