Одно из новых дополнений к ActiveRecord, которое будет выпущено с выпуском Rails 5, - это метод ActiveRecord :: Base.suppress.

У меня есть некоторая критика этой новой функции, и я хотел бы сформулировать ее в этом посте. Перед этим я хотел бы объяснить «рекламируемые» варианты использования этого нового метода, а также рассказать вам о его реализации. (что я считаю довольно крутым).

Когда использовать подавление

Хорошо, допустим, мы создаем приложение для социальной сети, в котором пользователи могут подписываться друг на друга. И когда кто-то подписывается на кого-то еще, мы отправляем уведомление следующему пользователю.

С точки зрения кода это выглядело бы примерно так:

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

Есть масса способов сделать это. Но поскольку этот блог посвящен методу Подавить, я покажу вам, как решить эту проблему с помощью этой новой функции.

Давайте изменим наш код и добавим новый метод follow_without_notification, использующий подавление -

Вот и все. Это решит наше требование.

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

Об этом мы поговорим в последней части поста.

Как это было реализовано

Исходный код этого нового метода был мне довольно интересен, и мне удалось извлечь из него несколько вещей. Итак, позвольте мне познакомить вас с кодом ActiveRecord :: Base.suppress.

Как видите, Suppressor - это модуль ActiveSupport :: Concern, который смешивается со всеми нашими моделями ActiveRecord в Rails.

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

Notification.suppress do 
  # stuff
end

Перейдем к методу подавления. В первой строке мы видим использование класса SuppressorRegistry, который определен в том же файле внизу. Если вы посмотрите на этот класс (строка 20), вы увидите, что это простой класс Ruby, расширяющий ActiveSupport :: PerThreadRegistry.

Quick Diversion - Я не хочу вдаваться в подробности в этом сообщении, но это, по сути, способ создать локальный поток глобальные данные в Rails. Если у вас есть класс в вашем коде, который расширяет ActiveSupport :: PerThreadRegistry, вы можете получить доступ к этому классу в любом месте вашего приложения, и когда вы это сделаете, вы получите одноэлементный объект этого класса в в котором вы можете хранить данные и при необходимости выражать поведение. Это просто хорошее улучшение Thread.current, и я бы рекомендовал прочитать этот пост Джастина Вайса по этой теме. Также я рекомендую вам прочитать исходный код этого, где вы увидите, как это реализовано поверх Thread.current.

В нашем случае SuppressorRegistry расширяет PerThreadRegistry, тем самым делая себя доступным глобально. И он определяет единственный атрибут под названием подавлен, инициализированный пустым хешем. (строка № 26).

Возвращаясь к определению метода подавления (строка № 7), мы видим следующую строку кода -

SuppressorRegistry.suppressed[name] = true

Таким образом, это в основном использует имя класса (полученное с помощью метода name), для которого было вызвано подавление, 'Уведомление' в нашем примере, в качестве ключа в подавленном хэше. и устанавливает для него значение true, чтобы подавленный хеш в SuppressorRegistry выглядел так:

{'Notification' => true}

И после этого он просто возвращает блок, который был передан в качестве аргумента, и поэтому весь наш код внутри блока выполняется. И как только блок завершается, выполнение переходит в блок sure (строка № 10), в котором значение ключа («Уведомление») обновляется до false.

Это все, что делает метод подавления. Настоящее действие находится в другом методе, который был определен в этом модуле. Это save! в строке 15. Это просто отменяет сохранение ActiveRecord :: Base! и это определяется как -

def save!(*args) 
  SuppressorRegistry.suppressed[self.class.name] ? self : super 
end

Ты понял? Теперь все становится на свои места. Вот если класс объекта, на котором сохраняем! вызывается (Notification.create в нашем примере), имеет флаг true в хэше подавлено, затем его заставляют тихо и просто вернуть объект. И если для класса нет флага true, вызывается super и выполнение продолжается как обычно.

Теперь, когда мы знаем, что на самом деле делает метод suppress, мы можем видеть, что когда мы это делаем,

Notification.suppress do 
  # stuff
end

это в основном блокирует все сохранение ActiveRecord ! методы, которые вызываются для экземпляров класса Notification внутри блока, переданного в suppress! метод.

Это действительно хорошее решение?

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

Почему? Начнем с проблемы GitHub, которая заставила задуматься. Поднимал его не кто иной, как DHH. В этом выпуске он написал о сценарии использования, который можно решить с помощью чего-то вроде метода подавления, и дал примерную идею реализации. Вот пример использования DHH.

Есть класс Comment, который при создании отправляет уведомления, создавая объекты Notification (аналогично моему примеру выше).

class Comment < ActiveRecord::Base
  belongs_to :commentable, polymorphic: true
  after_create -> { Notification.create! comment: self }
end

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

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

module Copyable
  def copy_to(destination)
    Notification.suppress do
      # Copy logic that creates new comments that we do not want triggering Notifications
    end
  end
end

Немного об этом.

  1. Как понять, что подавление должно влиять на ActiveRecord save !?

Последнее, что я проверял, код должен быть ясным и кратким. И правильное наименование вещей имеет большое значение для достижения этой цели. См. - IntentionRevealingNames.

Кроме того, представьте, что это широко используется в большой кодовой базе, и разработчику придется пройти через испытания, чтобы точно узнать, какие эффекты это имеет. Дело в том, что не сразу очевидно, что делает этот фрагмент кода. Необходимо выяснить, что это связано с Notification.create в обратном вызове after_create.

Ответ DHH на аналогичную проблему, поднятую на GitHub Кристианом Бикой:

Словарное определение, кажется, соответствует тому, что мы делаем для тройника: http://dictionary.reference.com/browse/suppress. Думаю, это работает со всеми классами. В своей базе кода я сейчас использую три разные версии этого в разных местах: Event.suppress, Mention.suppress, Notification.suppress. Все это работает на меня.

Я позволю вам судить об этом.

2. Насколько распространен этот вариант использования?

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

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

Хенрик Ню и Эллиот Винклер спорили о схожих вещах в номере и PR.

Итак, снова возникает вопрос: Действительно ли эта функция нужна в Rails?

3. Когда вы добавляете асинхронность?

Что, если создание объекта уведомления (которое, как мне кажется, является довольно странным способом обработки уведомлений) было запланировано на выполнение в фоновом режиме или, возможно, в отдельном потоке? Есть над чем подумать.

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

4. Слишком сильная связь

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

Например, почему модуль Copyable должен знать о классе с именем Уведомление?

Что, если кто-то создает в обратных вызовах много разных вещей? Тогда нам нужно будет сделать что-то вроде этого -

A.suppress do
  B.suppress do
    C.suppress do
      # do the stuff
    end
  end
end

Что продолжает добавлять зависимости. Вместо этого мы могли бы иметь метод / объект, представляющий эту концепцию (в котором A, B, C не нужно создавать), и использовать его.

Еще один веский аргумент был выдвинут Эллиотом Винклером в своем ответе, и я хотел бы процитировать его здесь -

если мы изменяем поведение Comment, почему мы должны указывать Notification что-то делать? Мы попадаем внутрь уведомлений из совершенно другого класса.

Я полностью согласен с этим аргументом.

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

Конечно, я в этом не специалист, поэтому могу ошибаться во многих вещах. Но когда я читал о новом дополнении, у меня сразу возникло неприятное предчувствие. А потом, когда я перешел к вопросу о GitHub и PR, я увидел, как многие люди спорят об одном и том же - это вроде как подтвердило мое первоначальное отношение к этому.

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

Я хотел бы знать, что вы думаете по этому поводу. Оставляйте отзывы!