Mit throw und catch den Code aufräumen

Angenommen, wir möchten eine einfache Controller-Action, die prüft, ob ein bestimmter Code gültig ist. Sie soll true oder false zurückgeben und, falls der Code ungültig ist, den Grund angeben (ob er unbekannt ist oder ob er bereits verwendet wurde). Die Action könnte so aussehen:

def check
  code = Code.find_by_value(params[:code])
  if code
    if code.used?
      respond_with do |format|
        format.json { render json: {valid: false, reason: "used"} }
      end
    else
      respond_with do |format|
        format.json { render json: {valid: true} }
      end
    end
  else
    respond_with do |format|
      format.json { render json: {valid: false, reason: "unknown"} }
    end
  end
end

Durch die tiefe Verschachtelung wird der eigentliche Algorithmus – in diesem Fall ein einfacher – effektiv versteckt. Drückt man das Ganze mit throw und catch aus, wird der Code etwas geradliniger:

def check
  result = catch(:result) do
    code = Code.find_by_value(params[:code])
    throw :result, {valid: false, reason: "unknown"} unless code
    throw :result, {valid: false, reason: "used"} if code.used?
    {valid: true}
  end
  respond_with do |format|
    format.json { render json: result }
  end
end

Wir sind bei einer einzigen respond_with-Zeile angekommen, aber das Hash-Merging und die throws am Zeilenanfang sind nicht besonders hübsch.

Führen wir ein kleines Ruby-Mixin für die Hash-Klasse ein, das ihr eine compact-Methode spendiert, die ihr Pendant Array schon immer hatte:

class Hash
  def compact
    delete_if { |k, v| v.nil? }
  end
end

Wenn wir damit den Response-Hash aufräumen und die throw-Anweisungen ans Ende verschieben, damit die einzelnen Schritte des Algorithmus sichtbar werden, erhalten wir die finale Version der Action:

def check
  result = catch(:result) do
    code = Code.find_by_value(params[:code])
    throw :result, reason: "unknown" unless code
    throw :result, reason: "used" if code.used?
    {valid: true}
  end
  respond_with do |format|
    format.json { render json: {valid: result[:reason].nil?, reason: result[:reason]}.compact }
  end
end