Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

ยกระดับ Ruby Rescue: Dynamic Exception Matching

เวลาใช้ rescue clause ใน Ruby คุณระบุ exception classes ที่ต้องการ rescue ได้ แค่ให้รายการ classes:


begin
  raise RuntimeError
rescue RuntimeError, NoMethodError
  puts "rescued!"
end

แต่ถ้าคุณไม่รู้ exception class ตอนเขียนโค้ด ล่ะ? คำตอบง่ายคือ rescue ทุก exceptions ทดสอบ แล้ว re-raise ที่ไม่ผ่าน ประมาณนี้:


begin
  raise "FUBAR! The ship's going down!"
rescue => e
  raise unless e.message =~ /^FUBAR/
  ... do something ...
end

แต่นี่น่าเบื่อ! และไม่ DRY จะสนุกกว่าถ้าบอก rescue clause ให้ rescue แค่ exceptions ที่ match conditions และ Ruby ทำได้!

Rescue match exceptions ยังไง?

เวลา exception เกิดใน rescue block Ruby interpreter ตรวจสอบ exception class ต่อรายการ classes ที่คุณให้ ถ้า match exception จะถูก rescue

Matching ประมาณนี้:


exception_classes_to_rescue.any? do |c|
  c === raised_exception.class
end

เหมือน operator ทุกตัวใน Ruby === เป็นเมธอดของ c ถ้าเรากำหนด === เอง จะทำอะไรได้บ้าง?

ตัวอย่างสร้างคลาส Anything ที่ Anything === x คืน true สำหรับค่า x ใดๆ ให้คลาสนี้เป็น argument ของ rescue จะ rescue ทุก exceptions:


class Anything
  def self.===(exception)
    true
  end
end

begin
  raise EOFError
rescue Anything
  puts "It rescues ANYTHING!"
end

แม้มีวิธี rescue ทุก exceptions ดีกว่านี้ โค้ดนี้น่าสนใจเพราะแสดงสองอย่าง:

  1. ให้ rescue clause classes ที่ไม่ inherit จาก Exception ได้ ถ้ามี ===
  2. ควบคุม === ได้ = ควบคุมได้ว่า rescue exceptions ไหน

Rescue exceptions ตาม message

ตอนรู้แล้ว เขียนโค้ด rescue แค่ exceptions ที่ message match pattern ง่าย:


class AllFoobarErrors
  def self.===(exception)
    # rescue ทุก exceptions ที่ message เริ่มด้วย FOOBAR
    exception.message =~ /^FOOBAR/
  end
end

begin
  raise EOFError, "FOOBAR: there was an eof!"
rescue AllFoobarErrors
  puts "rescued!"
end

Rescue exceptions ตาม custom attributes

เพราะเข้าถึง exception object matcher ใช้ข้อมูลใดๆ ภายใน object ได้

จินตนาการว่าคุณมี exception ที่มี custom attribute "severity" อยากกลืน "low severity" แต่วิ่ง "high severity" ผ่าน implement ได้แบบนี้:


class Infraction < StandardError
  attr_reader :severity
  def initialize(severity)
    @severity = severity
  end
end

class LowSeverityInfractions
  def self.===(exception)
    exception.is_a?(Infraction) && exception.severity == :low
  end
end

begin
  raise Infraction.new(:low)
rescue LowSeverityInfractions
  puts "rescued!"
end

ทำให้ Dynamic

ทั้งหมดคูลแต่มี boilerplate มาก มันมากเกินไปที่จะ define คลาส matcher เองทุกครั้ง เรา DRY ได้ด้วย metaprogramming นิดหน่อย

ตัวอย่างด้านล่าง define เมธอดสร้าง matcher classes ให้ คุณให้ matching logic ผ่าน block และ generator สร้างคลาสใหม่ที่ใช้ block ใน ===:


def exceptions_matching(&block)
  Class.new do
    def self.===(other)
      @block.call(other)
    end
  end.tap do |c|
    c.instance_variable_set(:@block, block)
  end
end

begin
  raise "FOOBAR: We're all doomed!"
rescue exceptions_matching { |e| e.message =~ /^FOOBAR/ }
  puts "rescued!"
end

ข้อควรระวัง

เหมือนหลาย cool tricks ใน Ruby ผมตัดสินใจไม่ได้ว่านี่บ้าหรือไอเดียดี อาจเป็นทั้งสอง แม้ผมไม่แนะนำให้ใช้เทคนิคนี้เป็น first choice แต่เห็นประโยชน์ในสถานการณ์ เช่น rescue exceptions ตาม severity ในทุกกรณี มันเครื่องมืออีกชิ้นในชุดเครื่องมือของคุณ!