まえがき
yield ってなにがどうなっているのか
(yeildが使われているコードの説明で) 自分で書くとこの設計できないのよね
こんな感想をいただきました
yield
の理解を深めてもらうために、こんな使い方をしているよというコードベースでご紹介していきます
対象読者
- yield がよくわからない人
- yield がわかっているけど、自分の書くコードで使ったことがない人
yield
ユースケース
パフォーマンス監視
処理時間を記録して報告するような機能
module XXXReportable
def with_reporting(name: )
started = Time.zone.now
yield
ended = Time.zone.now
report(name: name, started: started, ended: ended)
end
private
def report(name:, started:, ended:)
# どこか(主に外部サービス)に経過時間を記録する
end
end
# 使い方
class MyJob
include XXXReportable
def run
with_reporting(name: class.name) do
# メインとなる処理
end
end
end
メインとなる処理の前後に現在時刻を取得する処理が実行されるようになる
メインとなる処理完了後に report
が呼ばれる
原因がわからーんというときに限定的にログレベルを変える
def with_debug
old = Rails.logger.level
Rails.logger.level = :debug
yield
ensure
Rails.logger.level = old
end
# 使い方
# デバッグを有効にしたいメソッドを対象に
with_debug do
# something
end
リトライ機構
- 再実行可能な処理を
with_retry do ~ end
で囲む - 再実行可能なエラーがでたときに リトライ用の例外を飛ばす
class RetryError < StandardError; end
module Retriable
def with_retry(max_attempts: 5)
attempts = 0
begin
attempts += 1
yield
rescue RetryError => e
raise e.cause if max_attempts < attempts
retry
end
end
end
# 使い方
class MyJob
include Retriable
def run
with_retry do
do_something!
end
end
private
def do_something!
raise 'someerror'
rescue
# 特定の条件の時に RetryError を raiseさせる
# 例) Http接続ができなかったときなど
raise RetryError
end
end
MyJob.new.run
オブジェクトの設定項目が沢山あって辛い時
有名なやつ
- メインのクラスに
attr_accessor
が設定されるのを防ぐことができる - 設定内容の検証のために
valid?
とか用意しても辛くない
class Configuration
attr_accessor :max_attempts
attr_accessor :interval
def initialize
@max_attempts = 5
@interval = 10
end
end
class ApiClient
def initialize
yield configuration if block_given?
end
def invoke
puts configuration.max_attempts
puts configuration.interval
end
def configuration
@configuration ||= Configuration.new
end
end
client = ApiClient.new do |config|
config.max_attempts = 3
config.interval = 30
end
client.invoke
# => 3, 30
HTTPリクエストのハンドリングをさせる
# module にして includeして使うケースもある
class HttpHandler
def handle!
response = yield
raise MyErrors::Error::40X if response.status >= 400 && response.status < 500
# ...
JSON.parse(response.body)
end
end
# 使い方
client = ... # 適当な HttpClient
handler = HttpHandler.new
response = handler.handle! do
client.get '/path/to/resource'
end
APIの操作とレスポンスの処理のロジック分離
pagination があるようなAPIを想定
- APIにリクエストする
- レスポンス中の重要部分(課金データとか)の処理をする
- 次のページがあるならば繰り返し
class XXXApiClient
def invoke(path:, query: {})
query = { page: 1 }.merge(query)
loop do
response = http_client.get(path + query.to_params)
response['Items'].each do |item|
yield item
end
break unless next_page?(response)
query[:page] += 1
end
end
end
# 使い方
client = XXXApiClient
client.invoke(path: '/') do |item|
import!(item)
end
同一プロダクトのAPIではページネーション処理、エラー処理が共通化できることが多いため便利なことが多い
リモートから取得したデータはローカルに残さない
class RemoteCsvReader
def read(remote_path:, csv_options: {})
file = download!(remote_path)
CSV.foreach(file, csv_options) do |row|
yield row
end
ensure
file.delete if file && file.exist?
end
private
# @return [Pathname]
def download!(remote_path)
# リモート(S3など) からダウンロード
# TIPS: 実行ユーザーなどの情報をログに残したりしておく
end
end
# 使い方
reader = RemoteCsvReader.new
reader.read(remote_path: 'path/to/s3', csv_options: { col_sep: "\t" }) do |row|
# 各行ごとの処理
end
謝辞
レビューしてくれた人