Какова лучшая стратегия для обработки исключений и ошибок в Rails? - PullRequest
25 голосов
/ 09 апреля 2010

Мне было интересно, поделятся ли люди своими лучшими практиками / стратегиями обработки исключений и ошибок. Теперь я не спрашиваю, когда вызывать исключение (здесь было дано исчерпывающий ответ: SO: Когда выбрасывать исключение ). И я не использую это для своего потока приложений - но есть законные исключения, которые случаются постоянно. Например, самым популярным из них будет ActiveRecord :: RecordNotFound. Что было бы лучшим способом справиться с этим? СУХОЙ путь?

Прямо сейчас я много проверяю в своем контроллере, поэтому, если Post.find(5) возвращает Nil - я проверяю это и выкидываю флэш-сообщение. Однако, хотя это очень детально - это немного громоздко в том смысле, что мне нужно проверять наличие подобных исключений в каждом контроллере, в то время как большинство из них по сути одинаковы и имеют отношение к записи не найденной или связанной записи не найденной - такой либо Post.find(5) не найден, либо если вы пытаетесь отобразить комментарии, относящиеся к несуществующей записи, это вызовет исключение (что-то вроде Post.find(5).comments[0].created_at)

Я знаю, что вы можете сделать что-то подобное в ApplicationController и перезаписать его позже в конкретном контроллере / методе, чтобы получить более детальную поддержку, однако будет ли это правильным способом?

class ApplicationController < ActionController::Base
    rescue_from ActiveRecord::RecordInvalid do |exception|
        render :action => (exception.record.new_record? ? :new : :edit)
    end
end

Также это будет работать в случае, если Post.find(5) не найден, но как насчет Post.find(5).comments[0].created_at - я имел в виду, что не могу выбросить полное исключение, если сообщение существует, но не имеет комментариев, верно?

Подводя итог, я до сих пор делал много проверок вручную, используя if / else / instance или case / when (и, признаюсь, иногда начинаю / спасаю) и проверяя ноль? или пусто? и т. д., но, похоже, должен быть лучший способ.

ОТВЕТЫ: ​​

@ Милан: Привет милан Спасибо за ответ - я согласен с тем, что вы сказали, и я думаю, что неправильно использовал слово исключение. Я имел в виду, что сейчас я делаю много вещей, таких как:

if Post.exists?(params[:post_id])
    @p = Post.find(params[:post_id])
else
    flash[:error] = " Can't find Blog Post"
end

И я делаю много такого рода "обработки исключений", я стараюсь избегать использования begin / rescue. Но мне кажется, что это достаточно общий результат / проверка / ситуация, что для этого должен быть СУХОЙ способ, не так ли? Как бы вы сделали этот вид проверки?

Кроме того, как бы справиться с этим в этом случае? Допустим, вы хотите отобразить дату создания комментария в вашем представлении:

Last comment for this post at : <%= @post.comments[0].created_at %>

А у этого поста нет комментариев. Вы можете сделать

Last comment for this post at : <%= @post.comments.last.created_at unless @post.comments.empty? %>

Вы можете сделать проверку в контроллере. И т.д. Есть несколько способов сделать это. Но каков «лучший» способ справиться с этим?

Ответы [ 2 ]

14 голосов
/ 10 апреля 2010

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

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

Ваш второй пример (Post.find (5) .comments [0] .created_at) также не является исключительным. Некоторые посты просто не имеют комментариев, и вы знаете это заранее. Так почему же это исключение?

То же самое в случае с примером ActiveRecord :: RecordInvalid. Там просто нет причин обрабатывать этот случай с помощью исключения. То, что пользователь вводит в форму некоторые недействительные данные, довольно обычное дело, и в этом нет ничего исключительного.

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

С учетом сказанного это не означает, что вы не можете СУШИТЬ код, который инкапсулирует эти ситуации. Существует довольно большой шанс, что вы сможете сделать это хотя бы до некоторой степени, поскольку это довольно распространенные ситуации.

Итак, что насчет исключений? Что ж, первое правило на самом деле таково: используйте их как можно реже.

Если вам действительно нужно их использовать, есть два вида исключений в целом (на мой взгляд):

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

  2. исключения, которые вообще не позволяют пользователю использовать приложение. Они являются последним средством и должны обрабатываться через 500 внутренних ошибок сервера в веб-приложениях.

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

- РЕДАКТИРОВАТЬ -

Конструктивная часть:

Что касается первого, я бы посоветовал начать использовать find_by_id вместо find, поскольку он не выдает исключение, а возвращает nil в случае неудачи. Ваш код будет выглядеть примерно так:

unless @p = Post.find_by_id(params[:id])
  flash[:error] = "Can't find Blog Post"
end

что гораздо менее болтливо.

Другая распространенная идиома для СУШКИ в подобных ситуациях - использование контроллера before_filters для установки часто используемых переменных (например, @p в данном случае). После этого ваш контроллер может выглядеть следующим образом

controller PostsController
  before_filter :set_post, :only => [:create, :show, :destroy, :update]

  def show
      flash[:error] = "Can't find Blog Post" unless @p
  end 

private

  def set_post
    @p = Post.find_by_id(params[:id]) 
  end

end

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

# This is just your way of finding out the time of the last comment moved into a 
# helper. I'm not saying it's the best one ;)
def last_comment_datetime(post)
  comments = post.comments
  if comments.empty?
    "No comments, yet."
  else
    "Last comment for this post at: #{comments.last.created_at}"
  end
end

Тогда, по вашему мнению, вы просто позвоните

<%= last_comment_datetime(post) %>

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

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

1 голос
/ 10 апреля 2010

Исключения составляют исключительные обстоятельства. Плохой пользовательский ввод обычно не исключение; во всяком случае, это довольно часто. Когда у вас есть исключительные обстоятельства, вы хотите дать себе как можно больше информации. По моему опыту, лучший способ сделать это - неукоснительно улучшить обработку исключений на основе опыта отладки. Когда вы сталкиваетесь с исключением, самое первое, что вы должны сделать, это написать для него модульный тест. Второе, что вы должны сделать, это определить, есть ли дополнительная информация, которую можно добавить к исключению. Дополнительная информация в этом случае обычно принимает форму перехвата исключения выше по стеку и либо его обработки, либо создания нового, более информативного исключения, которое имеет преимущество дополнительного контекста. Мое личное правило - мне не нравится ловить исключения из более чем трех уровней вверх по стеку. Если исключение должно продвигаться дальше, вам нужно поймать его раньше.

Что касается выявления ошибок в пользовательском интерфейсе, операторы if / case вполне допустимы, если вы не вкладываете их слишком глубоко. Вот когда этот вид кода становится трудно поддерживать. Вы можете абстрагироваться, если это станет проблемой.

Например:

def flash_assert(conditional, message)
  return true if conditional
  flash[:error] = message
  return false
end

flash_assert(Post.exists?(params[:post_id]), "Can't find Blog Post") or return
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...