Подтвердить что ты не робот

Плюсы и минусы использования обратных вызовов для логики домена в Rails

Что вы видите как плюсы и минусы использования обратных вызовов для логики домена? (Я говорю в контексте Rails и/или Ruby-проектов.)

Чтобы начать обсуждение, я хотел бы упомянуть эту цитату на странице Mongoid по обратным вызовам:

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

Мне было бы интересно услышать аргумент или защиту по этому поводу. Предполагается ли оно применяться только к приложениям, поддерживаемым Mongo? Или он предназначен для применения через технологии баз данных?

Казалось бы, Руководство Ruby on Rails по проверке и обратным вызовам ActiveRecord может не совпадать, по крайней мере, когда речь заходит о реляционных базах данных. Возьмите этот пример:

class Order < ActiveRecord::Base
  before_save :normalize_card_number, :if => :paid_with_card?
end

По-моему, это прекрасный пример простого обратного вызова, реализующего логику домена. Это кажется быстрым и эффективным. Если бы я должен был взять совет Монгоиды, куда бы взялась эта логика?

4b9b3361

Ответ 1

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

before_save :ensure_values_are_calculated_correctly
before_save :down_case_titles
before_save :update_cache

Сразу видно, что происходит.

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

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

Callbacks

Преимущество обратных вызовов:

  • все находится в одном месте, поэтому упрощается
  • очень читаемый код

Недостаток обратных вызовов:

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

Наблюдатели

Преимущество наблюдателей

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

Недостаток наблюдателей

  • сначала было бы странно, как срабатывает поведение (смотрите в наблюдателя!)

Заключение

Короче говоря:

  • использовать обратные вызовы для простого материала, связанного с моделью (вычисленные значения, значения по умолчанию, проверки)
  • использовать наблюдателей для более сквозного поведения (например, отправки почты, состояния распространения,...)

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

Надеюсь, что это поможет.

Ответ 2

ИЗМЕНИТЬ: Я собрал ответы на рекомендации некоторых людей здесь.

Резюме

Основываясь на некоторых чтениях и размышлениях, я пришел к некоторым (предварительным) утверждениям того, что я считаю:

  • Утверждение "Использование обратных вызовов для логики домена - плохая практика проектирования" - это ложь, как написано. Это преувеличивает точку. Обратные вызовы могут быть хорошим местом для логики домена, которые используются надлежащим образом. Вопрос не должен быть, если логика модели домена должна идти в обратных вызовах, это то, что логика домена имеет смысл входить.

  • Утверждение "Использование обратных вызовов для логики домена... может привести к непредвиденным ошибкам, которые трудно отлаживать, когда обратные вызовы при остановке цепочки" истинно.

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

  • Да, вы должны иметь возможность протестировать свою бизнес-логику, не сохраняя объект в базе данных.

  • Если один объект обратного вызова слишком раздувается для вашей чувствительности, есть альтернативные варианты рассмотрения, в том числе (a) наблюдатели или (b) классы-помощники. Они могут обрабатывать операции с несколькими объектами.

  • Совет "только использовать [обратные вызовы] для сквозных проблем, например, в очереди на выполнение фоновых заданий", интригует, но завышен. (Я рассмотрел сквозные проблемы, чтобы убедиться, что я, возможно, что-то пропустил.)

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

Реакции на "обратные вызовы ActiveRecord разрушили мою жизнь"

Сообщение Mathias Meyer 2010, ActiveRecord Callbacks Ruined My Life, предлагает одну перспективу. Он пишет:

Всякий раз, когда я начинал добавлять проверки и обратные вызовы к модели в приложении Rails [...], это просто было ошибкой. Мне казалось, что я добавляю код, который не должен быть там, что делает все намного сложнее и превращается явным в неявный код.

Я считаю, что последнее требование "превращается в явный неявный код", должно быть, является несправедливым ожиданием. Мы говорим о Rails здесь, верно?! Большая часть добавленной стоимости связана с тем, что Rails делает вещи "магически", например. без участия разработчика явно. Разве не кажется странным наслаждаться плодами Rails и все же критически скрывать код?

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

Я согласен, что это звучит неприлично.

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

Да, это делает тестирование медленным и трудным.

Итак, в целом, я думаю, что Матиас добавляет кое-какое интересное топливо в огонь, хотя я не считаю все это убедительным.

Реакция на "Сумасшедший, Еретик и Удивительный: как я пишу Rails-приложения"

В публикации Джеймса Голика 2010, Сумасшедший, Еретик и Удивительный: как я пишу Rails Apps, он пишет:

Кроме того, связь всей вашей бизнес-логики с вашими объектами персистентности может иметь странные побочные эффекты. В нашем приложении, когда что-то создано, обратный вызов after_create генерирует запись в журналах, которые используются для создания фида активности. Что делать, если я хочу создать объект без ведения журнала - скажем, в консоли? Я не могу. Сохранение и ведение журнала состоят в браке навсегда и навсегда.

Позже он доберется до его корня:

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

Я очень ценю, что он смягчает свой совет, рассказывая вам, когда он применяется, а когда нет:

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

Ответ 3

Этот вопрос прямо здесь (Игнорировать ошибки проверки в rspec) - отличная причина, по которой вы не ставите логику в свои обратные вызовы: Testability.

Ваш код может иметь тенденцию к разработке многих зависимостей с течением времени, когда вы начинаете добавлять unless Rails.test? в свои методы.

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

Итак, в вашем случае я переместил бы normalize_card_number в before_validation, а затем вы можете проверить, что номер карты нормализован.

Но если вам нужно было уйти и создать файл PaymentProfile, я бы сделал это в другом объекте рабочего объекта:

class CreatesCustomer
  def create(new_customer_object)
    return new_customer_object unless new_customer_object.valid?
    ActiveRecord::Base.transaction do
      new_customer_object.save!
      PaymentProfile.create!(new_customer_object)
    end
    new_customer_object
  end
end

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

Ответ 4

Авди Гримм имеет несколько замечательных примеров в своей книге Object On Rails.

Здесь вы найдете здесь и здесь он не выбирает вариант обратного вызова и как вы можете избавиться от него просто путем переопределения соответствующего метода ActiveRecord.

В вашем случае вы получите что-то вроде:

class Order < ActiveRecord::Base

  def save(*)
    normalize_card_number if paid_with_card?
    super
  end

  private

  def normalize_card_number
    #do something and assign self.card_number = "XXX"
  end
end

[UPDATE после вашего комментария "это еще обратный вызов" ]

Когда мы говорим о обратных вызовах для логики домена, я понимаю обратные вызовы ActiveRecord, пожалуйста, исправьте меня, если вы считаете, что цитата из монгольского реферирования о чем-то еще, если есть "дизайн обратного вызова", где-то я его не нашел.

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

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

Что может быть худшим с ActiveRecord, вызывает его "общее использование" или "развязывающее чувство", которое они могут произвести. Первоначальная версия обратного вызова может показаться приятной, но поскольку вы добавите больше обратных вызовов, будет сложнее понять ваш код (в каком порядке они загружаются, что может остановить поток выполнения и т.д.) И протестировать его ( ваша логика домена связана с логикой устойчивости ActiveRecord).

Когда я читаю мой пример ниже, мне плохо об этом коде, он пахнет. Я считаю, что вы, вероятно, не закончили этот код, если вы делали TDD/BDD, и, если вы забыли о ActiveRecord, я думаю, вы просто написали метод card_number=. Я надеюсь, что этот пример достаточно хорош, чтобы напрямую не выбирать вариант обратного вызова и сначала думать о дизайне.

О цитате из MongoId Мне интересно, почему они советуют не использовать обратный вызов для логики домена, а использовать его для работы в очереди. Я думаю, что работа в очереди для очередей может быть частью логики домена и иногда может быть лучше разработана с чем-то другим, чем обратный вызов (скажем, Observer).

Наконец, есть некоторая критика о том, как ActiveRecord используется/реализуется с помощью Rail с точки зрения объектно-ориентированного программирования, этот ответ содержит хорошую информацию об этом и вы найдете более легко. Вы также можете проверить шаблон datamapper шаблон дизайна/проект реализации ruby, который может быть заменен (но насколько лучше) для ActiveRecord и не имеет его слабости.

Ответ 5

На мой взгляд, лучший сценарий использования обратных вызовов - это когда метод, запускающий его, не имеет ничего общего с тем, что выполняется в самом обратном вызове. Например, хороший before_save :do_something не должен выполнять код, относящийся к сохранению. Это больше похоже на работу Observer.

Люди склонны использовать обратные вызовы только для СУШКИ их кода. Это неплохо, но может привести к сложному и сложному поддержанию кода, потому что чтение метода save не говорит вам все, что он делает, если вы не заметите обратного вызова. Я думаю, что важно иметь явный код (особенно в Ruby и Rails, где так много волшебства).

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

Ответ 6

Я не думаю, что ответ слишком сложный.

Если вы намереваетесь создать систему с детерминированным поведением, обратные вызовы, связанные с данными, такими как нормализация, в порядке, обратные вызовы, которые касаются бизнес-логики, такие как отправка писем с подтверждением, не в порядке.

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

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

Теперь, как проектирует OO-системы с правильным балансом свободной связи и детерминированным поведением? Если вы знаете ответ, напишите книгу, я куплю ее! DCI, доменный дизайн, и в целом Шаблоны GoF - это начало: -)


  • http://www.artima.com/articles/dci_vision.html, "Где мы ошибались?". Не является основным источником, но согласуется с моим общим пониманием и субъективным опытом по-диким предположениям.