[Ruby] SingletonなLoggerクラスを作成する
アプリケーションのログを生成するクラスにはシングルトン・パターンがぴったりだから、Singletonモジュールを利用してシングルトン・パターンなロギングクラス(Loggerクラス継承)を作ろう、という趣旨の記事です。
この記事の要点
シングルトン・パターンなロギングクラスを作る・使うには、
-
Loggerクラスを継承したクラスを作成する
-
Singletonモジュールを
include
する -
initialize
メソッドを再定義する-
Logger.new
に渡す引数を指定したsuper
メソッドを呼ぶ(Logger.new
ではない!)
-
singleton_logger.rbrequire "logger" require "singleton" class SingletonLogger < Logger LOGFILE = "my_app.log" LOGLEVEL = Logger::Severity::INFO include Singleton def initialize super(LOGFILE, level: LOGLEVEL) end end
-
Singletonモジュールを
-
インスタンス生成時は、
instance
メソッドを呼ぶ(new
ではない!)main.rbrequire_relative "singleton_logger" logger = SingletonLogger.instance # これ! puts "hello" logger.info("said hello")
シングルトン・パターンとは?
シングルトン・パターンでは、アプリケーション内で、そのクラスのインスタンスを高々ひとつしか生成しないようにします。
既にそのクラスのインスタンスが生成されている場合は、新しいインスタンスを生成するのではなく、既存の(唯一の)インスタンスを渡すようにします。
デザインパターンの一種であり、いわゆるGoF本で定義されています。
Rubyでは、標準ライブラリであるSingletonモジュールを利用することで、簡単に実装できます。
そして、このシングルトン・パターンは、ログを生成するクラスと相性がよいのです。
ロギングクラスをシングルトン・パターンにすると何が嬉しい?
ロギングを考える際、「あのログファイルに記録する」「あのログレベル以上で記録する」は、アプリケーション内で共通・統一的であることが多いのではないでしょうか。
Ruby標準のloggerライブラリおよびLoggerクラスを単純に利用して、「特定のファイル・ログ記録レベル」にてログを出力したいとします。
この場合、正確に同じログファイル名と同じログ記録レベルをひとつひとつ指定して、モジュールやモジュールファイル(.rbファイル)ごとに、new
メソッドを繰り返す必要があります。
logger = Logger.new(log_file, level: log_level)
この方法には、以下のようなマイナス点があります:
- 単純に引数の指定の繰り返しが面倒くさい
- 引数の指定をミスして、アプリケーション内でログ生成環境が統一されない可能性がある
- (相対的に)インスタンス生成の数だけ重複して生成コストがかかる
- 「このログファイル名・ログ記録レベルを指定すること」というルールをどこかで運用・保守する必要がある
ここで、シングルトン・パターンにしたがうようにクラスを作成すると、
- 引数の指定ミスがなくなる(というか直接
new
できないので、インスタンス生成に関して余計なことができなくなる) - インスタンスの生成コストは初回の一度のみ
- 「あ、このインスタンス、アプリケーション内で統一なんだ」というのが一目でわかる
という形にすることができます。
シングルトン・パターンはコードデザインのやり方なので、どう実装してもよいのですが、Rubyでは標準ライブラリのSingletonモジュールを使うのが簡単・確実でしょう。
Singletonモジュール
Singletonモジュールは、シングルトン・パターンにしたいクラスの中でinclude
して使います。
Singletonモジュールをinclude
したクラスではnew
メソッドが隠蔽(privateメソッド化)され、かわりに
- アプリケーション内でまだインスタンスが存在しない場合、内部的に
new
を呼んでインスタンスを生成し、返す - インスタンスが既に生成済みの場合、そのインスタンスを返す
という動きをするinstance
メソッドで、インスタンスを呼び出します。
require "singleton"
class X
include Singleton
def initialize
puts "Xのインスタンスを生成しました"
end
end
# インスタンスの生成は一度きり
a = X.instance
#=> Xのインスタンスを生成しました
b = X.instance
#=> (出力なし)
c = X.instance
#=> (出力なし)
# すべて同一のインスタンス
p [a, b, c]
#=> [#<X:0x000055f7af906c60>, #<X:0x000055f7af906c60>, #<X:0x000055f7af906c60>]
インスタンスの生成は初回のinstance
での一度きりで、また2回目以降のinstance
で渡されているのがすべて同一のインスタンスである、ということがわかります。
ラッパーその他でよくない?
単純なnew
メソッドの隠蔽ならば、次のようなラッパーを定義すれば事足りるような気もします:
require "logger"
def my_logger
Logger.new("my_app.log", level: Logger::Severity::INFO)
end
# (使うとき)
logger = my_logger
logger.info("hello")
しかし、この方法では、my_logger
を呼ぶたびに相変わらず重複してインスタンス生成を行ってしまいます(まあ、実際に問題となるほど大したコストではないですが)。また、「絶対唯一のインスタンスを呼んでいる」というのが明示されないのも、ちょっとイケてないと思います。
この他にも、色々な方法が考えられますが、
- グローバル変数でいいじゃん => 色々と危ない
- クラス変数を活用すればいいじゃん => 初期化のタイミングがクラス定義時になる(Singletonモジュールの
instance
ではインスタンス生成時)、他のクラス変数やらと混ざってややこしくなる
のように、マイナスポイントがあります。詳しくは、例えば大分古いですが
などを参照してください(私が読んだのがこの本であるというだけで、似たようなことは他の本やサイトでも見つかると思います)。これらマイナスポイントに加えて、独自実装は保守コストがかかってくるので、素直に信頼性の高いSingletonモジュールを利用するのがよいと思います。
実装例
前置きが長くなりましたが、簡単な実装例に行きましょう。
今回は、以下のようなディレクトリ構成で、lib/singleton_logger.rbに、シングルトン・パターンなロギングクラスであるSingletonLoggerクラスを作成したいと思います。
また、SingletonLoggerインスタンスの呼び出し側として、main.rbとlib/another_file_moduleを作成しています。
.
├── lib
│ ├── singleton_logger.rb # <- SingletonLoggerクラスを定義
│ └── another_file_module.rb # <- 呼び出し側
├── log
│ └── my_day.log
└── main.rb # <- 呼び出し側
SingletonLoggerクラス
require "logger"
require "singleton"
class SingletonLogger < Logger
# ロギングパラメータ
LOGFILE = "log/my_day.log"
LOGLEVEL = Logger::Severity::INFO
include Singleton
def initialize
super(LOGFILE, level: LOGLEVEL)
end
end
単純にLoggerクラスを継承し、Singletonモジュールをinclude
しています。
初回のSingletonLogger.instance
呼び出し時に、内部的にSingletonLogger.new
が呼ばれます。
今回は「ファイル・ログレベルを指定したLoggerインスタンス」を返したいので、super
の引数にファイル名とログレベルを指定して、Logger.new
が呼ばれるようにしています。
ロギングパラメータをハードコードしているのがちょっと嫌な感じもしますが、シングルトン・パターンの概念上、生成されるインスタンスとクラスは一対一で密な関係であることを思えば、クラス定数ならばいいかなと思います。
呼び出し側
require_relative "lib/singleton_logger"
require_relative "lib/another_file_module"
def main
logger = SingletonLogger.instance # インスタンス呼び出し
puts "1時間だけゲームすることにします。"
logger.info("decided to play a game for 1 hour") # -> ログ書き出し
AnotherFileModule.play_more(hours: 3) # -> メソッド内部でログ書き出し
puts "きっちり切り上げて、夕飯を作ります!"
logger.info("decided to prepare dinner") # -> ログ書き出し
end
# 実行
main
require_relative "singleton_logger"
module AnotherFileModule
module_function
def play_more(hours: 1)
logger = SingletonLogger.instance # インスタンス呼び出し
puts "やっぱり、あと#{hours}時間だけゲームします。"
logger.info("decided to play the game #{hours} more hour(s)") # -> ログ書き出し
end
end
new
ではなく、instance
でインスタンス呼び出しをしているところがポイントです。
(本題とは関係ありませんが、このmain
メソッドを定義するやり方は、伊藤淳一さんの「Rubyスクリプトにもmainメソッドを定義するといいかも、という話」という記事を参考にしています。)
実行してみる
$ ruby main.rb
1時間だけゲームすることにします。
やっぱり、あと3時間だけゲームします。
きっちり切り上げて、夕飯を作ります!
$ cat log/my_day.log
# Logfile created on 2021-02-13 08:30:51 +0900 by logger.rb/v1.4.2
I, [2021-02-13T19:11:10.464850 #2769] INFO -- : decided to play a game for 1 hour
I, [2021-02-13T19:11:10.464987 #2769] INFO -- : decided to play the game 3 more hour(s)
I, [2021-02-13T19:11:10.465015 #2769] INFO -- : decided to prepare dinner
出力に関しては、ふぅん、そうだねという感じですね。
まとめ&なぜこの記事を書いたか
シングルトン・パターンなロギングクラスを作成しよう、というお話でした。
Rubyのシングルトン・パターンで調べると、Singletonモジュールを利用して独自のロギングクラスを作成する例は割と見かけるのですが、標準ライブラリのLoggerクラスを継承しているやり方はパッと見つからなかったので、記事にした次第です。
個人的には、Singletonモジュールを使ってみる過程で、以下の2点に少し悩みました:
- Loggerクラスを継承して作成しようとしているロギングクラスの
initialize
内で、とりあえずLogging.new
と書いてしまい、super
を使えばいいことにしばらく思い至らなかった
(多分、initialize
を特殊なものであると考えそうになっていた) - ラッパーでよくない?(1でちょっと詰まりかけた時に)
最終的には、この記事の実装例のような形ですっきりできたかな、と思っています。
デザインパターンやLoggerクラスについては理解が浅いので、「それアンチパターンやぞ」「それ、LoggerクラスのXXがダメになるよ」というのがありましたら、ご指摘いただけると嬉しいです。
Discussion