Nano Hash - криптовалюты, майнинг, программирование

Лучшая структура кода для ассоциаций Rails

Уровень

Давайте поговорим о наиболее распространенных типах ассоциаций, с которыми мы сталкиваемся.

У меня есть пользователь, который :has_many опубликовал(а)

class User < ActiveRecord::Base
  has_many :posts
end

class Post < ActiveRecord::Base
  belongs_to :user
end

Постановка задачи

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

Способ 1

Сделайте это в самом классе User.

class User < ActiveRecord::Base
  has_many :posts

  def process_posts
    posts.each do |post|
      # code of whatever 'process' does to posts of this user
    end
  end
end

Почтовый класс остается прежним:

class Post < ActiveRecord::Base
  belongs_to :user
end

Метод называется так:

User.find(1).process_posts

Почему это не лучший способ сделать это

Логика действий с сообщениями пользователя действительно должна принадлежать классу Post. В реальном мире пользователь также может иметь отношения :has_many со многими другими классами, например. orders, comments, children и т. д.

Если мы начнем добавлять похожие методы process_orders, process_comments, process_children (угу) в класс User, это приведет к одному гигантскому файлу с большим количеством кода, большая часть которого может (и должна) быть распределена туда, где она принадлежит, то есть в целевые ассоциации.

Способ 2

Прокси-ассоциации и области действия

Обе эти конструкции требуют добавления методов/кода в класс User, что снова делает его раздутым. Я бы предпочел, чтобы вся реализация была перенесена на целевые классы.

Способ 3

Метод класса для целевого класса

Создайте методы класса в целевом классе и вызовите эти методы для объекта User.

class User < ActiveRecord::Base
  has_many :comments
  # all target specific code in target classes
end

class Post < ActiveRecord::Base
  belongs_to :user

  # Class method
  def self.process
    Post.all.each do |post|  # see Note 2 below
      # code of whatever 'process' does to posts of this user
    end
  end
end

Метод называется так:

User.find(1).posts.process   # See Note 1 below

Теперь это выглядит лучше, чем методы 1 и 2, потому что:

  • Пользовательская модель остается свободной от беспорядка.
  • Функция процесса называется process вместо process_posts. Теперь мы можем иметь process и для других классов и вызывать их как: User.find(1).orders.process и т. д. вместо User.find(1).process_orders (Метод 1).

Примечание 1:

Да, вы можете вызвать такой метод класса в ассоциации. Почему читайте здесь. TL;DR заключается в том, что User.find(1).posts возвращает объект CollectionProxy, который имеет доступ к методам класса целевого (Post) класса. Он также удобно передает scope_attributes, в котором хранится user_id пользователя, вызвавшего posts.process. Это удобно. См. примечание 2 ниже.

Примечание 2:

Для тех, кто не знает, что происходит, когда мы выполняем Post.all.each в методе класса, он возвращает все сообщения пользователя, для которого был вызван этот метод, а не все сообщения в базе данных.

Поэтому при вызове как User.find(99).posts.process Post.all выполняет:

SELECT "notes".* FROM "posts" WHERE "posts"."user_id" = $1  [["user_id", 99]]

которые являются всеми сообщениями для идентификатора пользователя: 99.

Согласно комментарию @Jesuspc ниже, Post.all.each можно кратко записать как all.each. Это более идиоматично и не выглядит так, как будто мы запрашиваем все сообщения в базе данных.

Ответ, который я ищу

  • Объясняет, как лучше всего обращаться с такими ассоциациями. Как люди обычно это делают? и есть ли какие-либо очевидные конструктивные недостатки в методе 3.

  • Кстати, ваш метод self.process из третьего варианта может быть более идиоматичным, если вместо Post.all вы просто используете all. Post.all сбивает с толку, потому что похоже, что вы запрашиваете все сообщения (глобальная область), хотя на самом деле происходит то, что он запрашивает все элементы в данной области. 06.01.2016
  • @Jesuspc, хорошая мысль. Я добавил примечание в вопрос. 06.01.2016

Ответы:


1

Есть четвертый вариант. Полностью удалите эту логику из модели:

class PostProcessor
  def initialize(posts)
    @posts = posts
  end

  def process
    @posts.each do |post|
      # ...
    end
  end
end

PostProcessor.new(User.find(1).posts).process

Иногда это называют шаблоном Service Object. Очень приятный бонус этого подхода в том, что он делает написание тестов для этой логики действительно простым. Вот отличная запись в блоге об этом и других способах рефакторинга «толстых» моделей: http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/

06.01.2016
  • +1 согласен. Я упомянул (very light and quick) processing в вопросе, чтобы указать, что, возможно, несколько строк кода не могут полностью гарантировать новый класс. Тем не менее, хорошо, что вы указали на Четвертый способ. 06.01.2016

  • 2

    Лично я считаю, что метод 1 самый чистый. Будет очень чисто и понятно написать что-то вроде этого:

    Class User < ActiveRecord::Base
      has_many :posts
    
      def process_posts
        posts.each do |post|
         post.process
        end
      end
    end
    

    И поместите всю логику метода process в модель Post (с переменной экземпляра):

    Class Post < ActiveRecord::Base
      belongs_to :user
    
      def process
         # Logic of your Post process
      end
    end
    

    Таким образом, сама логика Post процесса принадлежит Post классу. Даже если ваша User модель будет иметь много "процессных" функций, они будут очень простыми и маленькими. Мне как разработчику это кажется очень чистым.

    Метод 3 имеет много технических последствий, которые довольно сложны и неинтуитивны (вам пришлось уточнить свой вопрос).

    ПРИМЕЧАНИЕ. Если вы хотите повысить производительность, возможно, вам следует использовать eager loading для уменьшения количества вызовов ActiveRecord, но это выходит за рамки этого вопроса.

    06.01.2016
  • Как пользователь «языка», вам не нужно иметь дело со сложностями метода 3. Под капотом в опубликованном вами коде происходит много сложных вещей. Пока вы имеете дело с абстракциями и у вас есть понятный интерфейс для работы, сложности базовой инфраструктуры не должны быть сдерживающим фактором в вашем решении использовать один метод вместо другого. Вещи, которые я (должен был) объяснить, были реализованы во фреймворке только для того, чтобы мы могли использовать тонкости, которые он дает нам как программистам на Rails. 06.01.2016
  • @NayaBonbo С моей (скромной) точки зрения сложности базовой структуры ДОЛЖНЫ быть сдерживающим фактором в вашем решении. Я пишу код, чтобы другие разработчики могли быстро прочитать и ознакомиться с ним. Использование тонкостей в моем коде не помогает. 06.01.2016
  • Но разве это не означает, что нельзя использовать расширенные возможности фреймворка или языка только потому, что это затрудняет понимание кода другими разработчиками, которые не знают язык так хорошо, как вы? Я понимаю, что вы пытаетесь сказать, хотя. Я предполагаю, что модерация является ключевым моментом, и, вероятно, не следует использовать загадочные функции, которые находятся за пределами понимания 90% разработчиков. 06.01.2016

  • 3

    Прежде всего, извините меня за самоуверенный ответ.

    Модели ActiveRecord — спорный вопрос. Его суть противоречит принципу единой ответственности, поскольку они обрабатывают как взаимодействие с базой данных через методы класса, так и объекты предметной области (которые используют для реализации собственного поведения) через свои экземпляры. В то же время они также нарушают принцип подстановки Лисков, поскольку модели не являются подвидами ActiveRecord::Base и реализуют собственный набор методов. И, наконец, парадигма ActiveRecord часто приводит к коду, нарушающему Закон Деметры, как в вашем предложении для третьего метода:

    User.find(1).posts.process
    

    Таким образом, существует тенденция, что для уменьшения связанности рекомендуется использовать объекты ActiveRecord только для взаимодействия с базой данных и поэтому не следует добавлять к ним какое-либо поведение (в вашем случае метод process). С моей точки зрения, это меньшее зло, хотя и не идеальное решение.

    Поэтому, если бы я реализовал то, что вы описываете, у меня был бы объект ProcessablePostsCollection (где имя Processable можно настроить, чтобы лучше описать, о чем идет речь, или даже полностью пренебречь, чтобы вы просто имели класс PostsCollection), который, вероятно, был бы обертка над списком сообщений с использованием SimpleDelegator и будет иметь метод process.

    class ProcessablePostsCollection < SimpleDelegator
      def self.from_collection(collection)
        new collection
      end
    
      def initialize(source)
        super source
      end
    
      def process
        # code of whatever 'process' does to posts
      end
    end
    

    И использование будет примерно таким:

    ProcessablePostsCollection.from_collection(User.find(1).posts).process
    

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

    Кроме того, если у вас есть большая таблица сообщений, вероятно, было бы разумно обрабатывать данные в пакетном режиме. Для этого ваш метод process может вызывать find_in_batches для ваших сообщений ActiveRecord::Relation.

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

    06.01.2016
  • +1 Спасибо за обзор. Сегодня я, конечно, озарился принципом единой ответственности и легией Деметры;) Ваш ответ хорошо согласуется с тем, что написал Джордан. Отдельный класс имеет смысл в зависимости от бизнес-логики того, что делает метод process. 06.01.2016

  • 4

    Вы не должны помещать это в модель User - поместите его в Post (если, конечно, объем process не включает модель User напрямую):

    #app/models/post.rb
    class Post < ActiveRecord::Base
        def process
           return false if post.published?
           # do something
        end
    end
    

    Затем вы можете использовать расширение ассоциации ActiveRecord, чтобы добавить функциональность к модели User:

    #app/models/user.rb
    class User < ActiveRecord::Base
       has_many :posts do
          def process
              proxy_association.target.each do |post|
                 post.process
              end
          end
       end
    end
    

    Это позволит вам звонить...

    @user = User.find 1
    @user.posts.process
    
    06.01.2016
  • +1 Это «способ Rails» для расширения функциональности ассоциации. Спасибо, что показали точный код для этого. Это будет полезно для людей, которые хотят придерживаться предписанного способа делать такие вещи в Rails. Моя проблема с использованием расширений заключается в том, что пользовательский класс будет загроможден логикой различных разрозненных классов (на случай, если нам понадобятся расширения для многих связанных классов). Для одноразового расширения, которое имеет несколько строк реализации, это полностью работает. Интересно, что синтаксис вызова в этом методе такой же, как и в методе 3 — @user.posts.process. 07.01.2016
  • Новые материалы

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

    Как написать эффективное резюме
    Предложения по дизайну и макету, чтобы представить себя профессионально Вам не позвонили на собеседование после того, как вы несколько раз подали заявку на работу своей мечты? У вас может..

    Частный метод Python: улучшение инкапсуляции и безопасности
    Введение Python — универсальный и мощный язык программирования, известный своей простотой и удобством использования. Одной из ключевых особенностей, отличающих Python от других языков, является..

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

    Работа с векторными символическими архитектурами, часть 4 (искусственный интеллект)
    Hyperseed: неконтролируемое обучение с векторными символическими архитектурами (arXiv) Автор: Евгений Осипов , Сачин Кахавала , Диланта Хапутантри , Тимал Кемпития , Дасвин Де Сильва ,..

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

    Обеспечение масштабируемости LLM: облачный анализ с помощью AWS Fargate и Copilot
    В динамичной области искусственного интеллекта все большее распространение получают модели больших языков (LLM). Они жизненно важны для различных приложений, таких как интеллектуальные..