やりたいこと
YAML に書かれた設定値を読み込む AppData
クラスがあります。
---
version: 0.9.9
class AppData
attr_reader :version
def initialize
@version = yaml.fetch('version')
end
private
def yaml = YAML.load_file(Pathname(__dir__).join('config/app.yml'))
end
途中で YAML を更新しても、インスタンスを生成しなおせば AppData#version
が更新されます。
app_data = AppData.new
app_data.version
#=> "0.9.9"
# config/app.yml の 0.9.9 を 1.0.0 に更新する。
puts(Pathname(__dir__).join('config/app.yml').read)
# ---
# version: 1.0.0
app_data.version
#=> "0.9.9"
app_data = AppData.new
app_data.version
#=> "1.0.0"
では AppData
が Singleton モジュールを include している場合はどうでしょう?
class AppData
include Singleton
attr_reader :version
def initialize
@version = yaml.fetch('version')
end
private
def yaml = YAML.load_file(Pathname(__dir__).join('config/app.yml'))
end
app_data = AppData.instance
app_data.version
#=> "0.9.9"
# config/app.yml の 0.9.9 を 1.0.0 に更新する。
puts(Pathname(__dir__).join('config/app.yml').read)
# ---
# version: 1.0.0
app_data.version
#=> "0.9.9"
# version が更新されない。
app_data = AppData.instance
app_data.version
#=> "0.9.9"
途中で YAML を更新すると、AppData.instance
でインスタンスを取得して直しても古いバージョンのままです。(Singleton パターンを使っているので当たり前ではありますが) インスタンスが最初に AppData.instance
を呼んだ時の状態のままだからですね。新しい Ruby プロセスでインスタンス化すれば AppData#version
を更新できます。
# irb や Ruby アプリケーションを再起動して、新しい Ruby プロセスでインスタンス化する。
app_data = AppData.instance
app_data.version
#=> "1.0.0"
では同じ Ruby プロセスのままで「Singleton モジュールを include したクラスのインスタンスの状態を更新する」ということはできないでしょうか? 🤔
方法
Singleton.__init__ を使うと可能です。引数に Singleton なクラスを指定します。
app_data = AppData.instance
app_data.version
#=> "0.9.9"
# config/app.yml の 0.9.9 を 1.0.0 に更新する。
puts(Pathname(__dir__).join('config/app.yml').read)
# ---
# version: 1.0.0
app_data = AppData.instance
app_data.version
#=> "0.9.9"
# Singleton なクラスである AppData の唯一のインスタンスを初期化する。
Singleton.__init__(AppData)
#=> AppData
# version が更新された!
app_data = AppData.instance
app_data.version
#=> "1.0.0"
Singleton.__init__
の実装は以下のとおりです。インスタンスをスレッドセーフに初期化しています。
# https://github.com/ruby/singleton/blob/v0.3.0/lib/singleton.rb#L162-L168
def __init__(klass) # :nodoc:
klass.instance_eval {
set_instance(nil)
set_mutex(Thread::Mutex.new)
}
klass
end
ただし、ソースコード中に :nodoc:
とコメントされている様に、これはドキュメント化されていない (undocumented) メソッドです。Singleton::VERSION
の変更に伴い、内部実装が変わってこのメソッドの挙動が変わったりメソッド自体が廃止されたりする可能性もあるので注意してください。
バージョン情報
RUBY_VERSION
#=> "3.4.6"
Singleton::VERSION
#=> "0.3.0"