Хорошо ли использовать исключения для потока управления в Ruby или Ruby on Rails?

Я читаю Agile Web Development с Rails (4-е изд.), и я нашел следующий код

class ApplicationController < ActionController::Base
  protect_from_forgery

  private

  def current_cart
    Cart.find(session[:cart_id])
  rescue ActiveRecord::RecordNotFound
    cart = Cart.create
    session[:cart_id] = cart.id
    cart
  end
end

Поскольку я разработчик Java, мое понимание этой части кода более или менее следующее:

private Cart currentCard(){
  try{
    return CartManager.get_cart_from_session(cartId)
  }catch(RecordNotFoundEx e){
    Cart c = CartManager.create_cart_and_add_to_session(new Cart())
    return c;    
  }
}

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

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

Есть ли веская причина, почему такая практика оправдана в Ruby (Rails)? Это обычная практика в Ruby?

Ответы

Ответ 1

Рельсы никоим образом не согласуются в использовании исключений. find вызовет исключение, если объект не найден, но для сохранения вы можете выбрать, какое поведение вы хотите. Наиболее распространенная форма:

if something.save
  # formulate a reply
else
  # formulate an error reply, or redirect back to a form, or whatever
end

то есть. save возвращает true или false. Но есть также save!, который вызывает исключение (добавление восклицательного знака в конец имени метода - это рубизм, показывающий, что метод "опасен" или разрушителен или просто имеет побочные эффекты, точную значение зависит от контекста).

Существует веская причина, почему find вызывает исключение: if a RecordNotFound исключение пузырьков до верхнего уровня, это вызовет рендеринг страницы 404. Поскольку вы обычно не используете эти исключения вручную (редко, вы видите rescue ActiveRecord::RecordNotFound в приложении Rails), вы получаете эту функцию бесплатно. В некоторых случаях, однако, вы хотите что-то сделать, когда объект не существует, и в этих случаях вам нужно поймать исключение.

Я не думаю, что термин "наилучшая практика" на самом деле означает что-то, но мой опыт показывает, что исключения больше не используются для контроля потока в Ruby, чем в Java или любом другом языке, который я использовал. Учитывая, что Ruby не имеет проверенных исключений, вы имеете дело с исключениями гораздо меньше в целом.

В конце концов, до интерпретации. Поскольку наиболее распространенным вариантом использования для find является получение объекта для его отображения и что URL-адрес для этого объекта будет сгенерирован приложением, это может быть исключительным обстоятельством, что объект не может быть найден. Это означает, что либо приложение генерирует ссылки на объекты, которые не существуют, либо что пользователь вручную редактировал URL-адрес. Также может быть, что объект был удален, но ссылка на него все еще существует в кеше или через поисковую систему, я бы сказал, что это тоже исключительное обстоятельство.

Этот аргумент применяется к find при использовании как в вашем примере, то есть с идентификатором. Существуют и другие формы find (включая многие варианты find_by_*), которые фактически выполняют поиск, и те, которые не создают исключений (а затем в Rails 3 есть where, что заменяет многие из использования find в Rails 2).

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

Ответ 2

Для конкретного варианта использования вы можете просто сделать

def current_cart
  cart = Cart.find_or_create_by_id(session[:cart_id])
  session[:cart_id] = cart.id
  cart
end

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

Ответ 3

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

def current_cart
  @current_cart ||= begin
    unless cart = Cart.find_by_id(session[:cart_id])
      cart = Cart.create
      session[:cart_id] = cart.id
    end
    cart
  end
end