Spec-Zone.ru › Ruby on Rails 8.1

модуль 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.

Открытые методы экземпляра

concern (topic, &module_definition) Показать исходный код
# 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
concerning (topic, prepend: false, &block) Показать исходный код
# 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.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API