Cách "try again" khi gặp exception trong Ruby

Trong lập trình, chắc hẳn bạn đã không ít lần gặp những lỗi "khó đỡ" mà cách giải quyết duy nhất là chạy lại đoạn code một lần nữa. May thay, các lập trình viên Ruby được cung cập một công cụ để xử lý tình huống này. Bài viết này sẽ nghiên cứu về cơ chế này và cách mà nó hoạt động.

Từ khóa "retry"

Từ khóa "retry" được build sẵn trong cấu trúc rescue của Ruby. Cách sử dụng "retry" rất đơn giản: Chỉ cần đặt nó vào trong rescue block sẽ giúp cho chương trình chạy lại đoạn code ở block trước đó.

begin
  retries ||= 0
  puts "try ##{ retries }"
  raise "the roof"
rescue
  retry if (retries += 1) < 3
end

# ... outputs the following:
# try #0
# try #1
# try #2

Những điều cần lưu ý khỉ sử dụng "retry":

  • Khi chương trình chạy đến "retry", tất cả đoạn code từ "begin" đến "rescue" sẽ được chạy lại từ đầu.
  • Nếu không có cơ chế để giới hạn số lần "retry", rất có thể chương trình sẽ rơi vào vòng lặp vô hạn.
  • Code trong cả block "begin" và "rescue" đều có thể truy cập đến các biến chung của block cha.

Các vấn đề với "retry"

Với cách đặt giới hạn hợp lý, "retry" trở thành một công cụ hữu dụng. Vấn đề chính ở đây là đôi khi chạy lại cả một block không phải là một ý hay.

Ví dụ như trong trường hợp sử dụng một gem hoặc service giúp ta post bài lên Twitter, Facebook,... chỉ bằng một hàm, code của ta sẽ giống như phía dưới:

SocialMedia.post_to_all("Zomg! I just ate the biggest hamburger")

# ...posts to Twitter API
# ...posts to Facebook API
# ...etc

Nếu một trong các API bị lỗi, chương trình sẽ raise exception SocialMedia::TimeoutError. Nếu ta bắt exception và retry, các API đã post thành công sẽ phải post lại lần nữa. Chắc chắn không ai đồng ý với ý tương này.

begin
  SocialMedia.post_to_all("Zomg! I just ate the biggest hamburger")
rescue SocialMedia::TimeoutError
  retry
end

# ...posts to Twitter API
# facebook error
# ...posts to Twitter API
# facebook error
# ...posts to Twitter API
# and so on

Để không phải đặt những điều kiện, tham số phức tạp, ta phải tìm được một cách nào đó để retry lại block từ dòng lệnh lỗi chứ không phải từ đầu begin. Rất may mắn là Ruby hoàn toàn cho phép ta làm điều đó.

Chú ý: Tất nhiên giải pháp chuẩn của vấn đề này là viết lại thư viện kết nối. Nhưng đối với những người không hiểu rõ về thư viện hoặc những người lười (như tôi) thì đây không phải một ý hay.

Continue trong khối rescue

Trong khối "begin" ta có thể đặt một vài "save point", giống như trong game. Ta có thể đi ra và làm các công việc khác, rồi khi muốn thì "load" lại "save point" và "chơi" tiếp. Dưới đây là một ví dụ:

require "continuation"
counter = 0
continuation = callcc { |c| c } # create savepoint
puts(counter += 1)
continuation.call(continuation) if counter < 5 # jump back to savepoint

Mỗi lần quay trở về save point, các biến của continuation sẽ là các tham số truyền vào trong hàm "call". Vì vây, ta nên dùng cú pháp continuation.call(continuation)

Thêm continuation vào exception

Bây giờ ta sẽ sử dụng continuation để thêm các phương thức skip vào các exception. Đoạn code dưới là những gì ta đang mong muốn thực hiện. Khi một exception bị raise lên, hàm e.skip làm cho code tiếp tục chạy tiếp như không có gì sảy ra.

begin
  raise "the roof"
  puts "The exception was ignored"
rescue => e
  e.skip
end

# ...outputs "The exception was ignored"

Để làm được điều này, ta cần thêm hàm vào class Exception:

class Exception
  attr_accessor :continuation
  def skip
    continuation.call
  end
end

Bây giờ ta sẽ định nghĩa continuation cho các exception:

require 'continuation'
module StoreContinuationOnRaise
  def raise(*args)
    callcc do |continuation|
      begin
        super
      rescue Exception => e
        e.continuation = continuation
        super(e)
      end
    end
  end
end

class Object
  include StoreContinuationOnRaise
end

Bây giờ ta có thể skip các exception và chạy tiếp code như chưa hề có cuộc chia li! Tôi đã thành công còn bạn thì sao?


All Rights Reserved