модуль Module::Concerning
Небольшие модули для разделения ответственности
Нередко у нас появляется фрагмент поведения среднего размера, который хотелось бы вынести отдельно, но подключить только к одному классу.
Часто хорошим решением будет создать обычный объект Ruby, чтобы инкапсулировать это поведение и взаимодействовать с исходным объектом или делегировать ему вызовы. Но если нет дополнительного состояния, которое нужно инкапсулировать, или мы объявляем DSL-конструкции для родительского класса, новые объекты-посредники могут скорее запутать, чем упростить код.
Обычно всё просто помещают в один монолитный класс, возможно, добавив комментарий, — как наименее плохой вариант. Если же разнести модули по отдельным файлам, придётся утомительно изучать их, чтобы составить целостное представление.
Неудачные способы разделения небольших задач
С помощью комментариев:
class Todo < ApplicationRecord
# Other todo implementation
# ...
## Event tracking
has_many :events
before_create :track_creation
private
def track_creation
# ...
end
end
С помощью встроенного модуля:
Слишком много лишнего синтаксиса.
class Todo < ApplicationRecord
# Other todo implementation
# ...
module EventTracking
extend ActiveSupport::Concern
included do
has_many :events
before_create :track_creation
end
private
def track_creation
# ...
end
end
include EventTracking
end
Перенос лишнего кода примеси в отдельный файл:
Когда фрагмент поведения становится настолько большим, что для его понимания приходится прокручивать страницу, мы сдаёмся и переносим его в отдельный файл. При таком размере дополнительные накладные расходы могут быть разумным компромиссом, даже если теперь сложнее с первого взгляда понять, как всё устроено.
class Todo < ApplicationRecord # Other todo implementation # ... include TodoEventTracking end
Знакомьтесь: Module#concerning
Если устранить лишний синтаксис примесей, получится естественный и простой способ выделять небольшие задачи в отдельные модули.
class Todo < ApplicationRecord
# Other todo implementation
# ...
concerning :EventTracking do
included do
has_many :events
before_create :track_creation
end
private
def track_creation
# ...
end
end
end
Todo.ancestors
# => [Todo, Todo::EventTracking, ApplicationRecord, Object]
Это небольшое изменение даёт несколько замечательных преимуществ. Теперь можно:
-
сразу понять, как устроено поведение класса;
-
привести в порядок монолитные классы-свалки, разделив их задачи; и
-
перестать использовать protected/private как грубый способ обозначить модульность «внутреннего кода».
Добавление concerning в начало
concerning поддерживает аргумент prepend: true, который будет prepend задачу вместо использования для неё include.
Открытые методы экземпляра
# File activesupport/lib/active_support/core_ext/module/concerning.rb, line 132
def concern(topic, &module_definition)
const_set topic, Module.new {
extend ::ActiveSupport::Concern
module_eval(&module_definition)
}
end Короткий способ определить задачу без лишнего кода.
concern :EventTracking do ... end
эквивалентно
module EventTracking extend ActiveSupport::Concern ... end
# File activesupport/lib/active_support/core_ext/module/concerning.rb, line 114 def concerning(topic, prepend: false, &block) method = prepend ? :prepend : :include __send__(method, concern(topic, &block)) end
Определяет новую задачу и подключает её как примесь.
© 2004–2021 David Heinemeier Hansson
Licensed under the MIT License.