Одно из новых дополнений к 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
Немного об этом.
- Как понять, что подавление должно влиять на 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 открыто поощрял такие вещи и приводил новичков в беду.
Я хотел бы знать, что вы думаете по этому поводу. Оставляйте отзывы!