Применение того, как ActiveRecord использует модули к другим проектам Ruby - PullRequest
3 голосов
/ 07 марта 2012

Я решил покопаться в коде ActiveRecord для Rails, чтобы попытаться выяснить, как некоторые из них работают, и был удивлен, увидев, что он состоит из множества модулей, которые, похоже, все включены в ActiveRecord :: Base.

Я знаю, что модули Ruby предоставляют средства для группировки связанных и повторно используемых методов, которые можно смешивать с другими классами для расширения их функциональности.

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

Это заставило меня задуматься о том, как разработан ActiveRecord и как эту логику можно или нужно применять к другим приложениям Ruby.

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

В тех случаях, когда у класса может быть много методов, и было бы неудобно иметь их все в одном файле, имеет ли смысл просто заново открыть класс в других файлах и определить в нем больше методов?

В приложении командной строки, над которым я работаю, у меня есть несколько классов, которые выполняют различные функции, но у меня есть класс верхнего уровня, который предоставляет API для всего приложения - я обнаружил, что этот класс увязает с множество методов, которые действительно передают работу другому классу, и подобны клеям, которые скрепляют части приложения. Думаю, мне интересно, имеет ли смысл разделять некоторые связанные методы на модули или заново открывать класс в разных файлах кода? Или есть что-то еще, о чем я не думаю?

1 Ответ

3 голосов
/ 07 марта 2012

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

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

module Aggregator
  def aggregate
    unless respond_to?(:collection)
      raise Exception.new("Classes including #{self} must define #collection")
    end
    # ...
  end
end

Я бы не решался зависеть от переменных общего экземпляра.

Самая большая проблема, которую я вижу при повторном открытии классов, - это просто управление исходным кодом. Вы бы в конечном итоге с несколькими копиями aggregator.rb в разных каталогах? Определяется ли порядок загрузки этих файлов и влияет ли это на переопределение или вызов методов в классе? По крайней мере, для модулей порядок включения в источнике явный.


Обновление: В комментарии Стивен спросил о тестировании модуля, который должен быть включен в класс.

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

# spec/shared_examples/aggregator_examples.rb
shared_examples_for 'Aggregator' do
  describe 'when aggregating records' do
    it 'should accumulate values' do
      # ...
    end
  end
end

# spec/models/model_spec.rb
describe Model
  it_should_behave_like 'Aggregator'
end

Даже если вы не используете RSpec, вы все равно можете создать простой класс-заглушку, включающий ваш модуль, а затем написать тесты для экземпляров этого класса:

# test/unit/aggregator_test.rb
class AggregatorTester
  attr_accessor :collection

  def initialize(collection)
    self.collection = collection
  end

  include Aggregator
end

def test_aggregation
  assert_equal 6, AggregatorTester.new([1, 2, 3]).aggregate
end
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...