Edited at

[Ruby] DelegateClass を使って僕だけの Logger を実装する (追記あり)


標準出力に出力するための Logger オブジェクトを作りたい :raised_hands_tone1:

require 'logger'

logger = Logger.new(STDOUT)
logger.formatter = Logger::Formatter.new
logger.datetime_format = '%Y/%m/%d %H:%M:%S '

logger.info('BONFIRE LIT')
logger.error('YOU DIED')

I, [2019-07-09T21:40:12.663123 #18929]  INFO -- : BONFIRE LIT

E, [2019-07-09T21:40:13.036144 #18929] ERROR -- : YOU DIED

このような形で毎回 logger オブジェクトを作るのは大変だから、クラス化しよう :smirk_cat:

require 'forwardable'

require 'logger'

class StdoutLogger
extend Forwardable

def_delegators :logger, :debug, :info, :warn, :error, :fatal

private

def logger
@logger ||=
Logger.new(STDOUT).tap do |logger|
logger.formatter = Logger::Formatter.new
logger.datetime_format = '%Y/%m/%d %H:%M:%S '
end
end
end

logger = StdoutLogger.new
logger.info('BONFIRE LIT')
logger.error('YOU DIED')

以前はこのように「Logger オブジェクトである logger を所有し、infoerror などのメソッドを logger に委譲する」というクラスを実装していました。しかし、この方法では委譲したいメソッドが増えた場合、例えば Logger#unknown も委譲したい場合などに def_delegators に追記しなくてはいけません :weary:

そこで便利なのが Kernel#DelegateClass です。

require 'delegate'

require 'logger'

class StdoutLogger < DelegateClass(Logger)
def initialize
super(logger)
end

private

def logger
@logger ||=
Logger.new(STDOUT).tap do |logger|
logger.formatter = Logger::Formatter.new
logger.datetime_format = '%Y/%m/%d %H:%M:%S '
end
end
end

logger = StdoutLogger.new
logger.info('BONFIRE LIT')
logger.error('YOU DIED')

DelegateClass(Logger) を継承することで、Logger のインスタンスメソッドはすべて logger に委譲されます。

ちなみに Logger クラスを継承する方法では


最後に

DelegateClass は非常に便利で、例えば Array のラッパークラスを定義したい場合にも使えます。これについては以前書いた

という記事を参照してください。

Logger 関連でよろしければこちらの記事もご覧ください。


追記: 通常の継承の方がいいような……

この場合 Logger クラスを継承する方法でもいいような……。

require 'logger'

class StdoutLogger < Logger
def initialize
super(STDOUT, formatter: Formatter.new, datetime_format: '%Y/%m/%d %H:%M:%S ')
end
end

logger = StdoutLogger.new
logger.info('BONFIRE LIT')
logger.error('YOU DIED')

違いのひとつとして、Logger クラスのサブクラスかどうかが異なる。

# DelegateClass(Logger) を継承する場合

logger.is_a?(Logger)
#=> false

# Logger を継承する場合
logger.is_a?(Logger)
#=> true

この場合、むしろ Logger オブジェクトであると見なしたほうが適切かもしれない。