シングルトンテーブル
CoddyのLuaジャーニー「Object Oriented Programming」セクションの一部 — レッスン 63/70。
プログラム全体で、設定マネージャー、ロギングシステム、ゲーム設定など、あるもののインスタンスが正確に1つだけ必要な場合があります。複数のコピーを作成すると、どれが正しいデータを持っているのかという問題が発生します。Singletonパターンは、インスタンスが1つしか存在しないことを保証します。
Luaでは、モジュールの仕組みのおかげで、シングルトンの実装は驚くほど簡単です。モジュールを require すると、Luaはその結果をキャッシュします。その後 require を呼び出すと、新しいテーブルではなく、キャッシュされたテーブルが返されます。
-- GameConfig.lua
local GameConfig = {
volume = 80,
difficulty = "normal"
}
function GameConfig:setVolume(v)
self.volume = v
end
return GameConfig
これで、プログラム内のどこからでも:
local config1 = require("GameConfig")
local config2 = require("GameConfig")
config1:setVolume(50)
print(config2.volume) -- 50 (同じテーブルです!)
config1 と config2 の両方が、まったく同じテーブルを参照しています。一方の変数を通じて行われた変更は、もう一方の変数からも確認できます。これは、Lua の require が戻り値を package.loaded に保存して再利用するため、自動的に発生します。
新しいオブジェクトを作成するFactoryパターンとは異なり、Singletonはアプリケーション全体で共有された状態を保証します。これは、コードの複数の箇所から一貫してアクセスや変更を行う必要があるグローバルな設定に最適です。
チャレンジ
簡単シングルトンパターンを使用して、ゲーム設定マネージャーを構築しましょう!プログラムのどの部分から呼び出されても、まったく同じ設定にアクセスできるようにする設定モジュールを作成します。何度 require されても、常に同じインスタンスが返されます。
コードは2つのファイルに分けて構成します:
GameSettings.lua: ゲーム設定を保存するシングルトンモジュールを作成します。設定テーブルには、musicVolume(初期値 70)、sfxVolume(初期値 100)、およびdifficulty(初期値"normal")を含める必要があります。これらの設定を変更するために、:setMusicVolume(value)、:setSfxVolume(value)、:setDifficulty(level)の3つのメソッドを追加してください。モジュールの最後でテーブルを返すことを忘れないでください!main.lua: GameSettings モジュールを2つの異なる変数にrequireすることで、シングルトンが機能することを証明します。1つ目の変数を使用して、音楽の音量と難易度を変更します。次に、2つ目の変数を使用して3つの設定すべてをプリントします。シングルトンが正しく機能していれば、両方の変数は同じテーブルを参照しているため、一方で行った変更がもう一方にも反映されます。
2つの入力を受け取ります:
- 新しい音楽の音量(数値)
- 新しい難易度レベル(
"easy"や"hard"などの文字列)
1つ目の変数を通じて設定を変更した後、2つ目の変数を使用して3つの設定を、音楽の音量、SFXの音量、難易度の順に、それぞれ個別の行にプリントしてください。
例えば、入力が 50 と hard の場合、出力は以下のようになります:
50
100
hard入力が 25 と easy の場合、出力は以下のようになります:
25
100
easySFXの音量は変更していないため、デフォルト値の 100 のままですが、音楽の音量と難易度は1つ目の変数を通じて行われた変更を反映しています。これにより、両方の変数が同じシングルトンインスタンスを指していることが証明されます!
チートシート
シングルトンパターンは、プログラム全体を通してオブジェクトのインスタンスが1つだけ存在することを保証します。これは、複数のインスタンスが存在するとデータの不整合が生じる可能性がある設定マネージャー、ロギングシステム、またはゲーム設定などに役立ちます。
Luaでは、シングルトンパターンはモジュールシステムを通じて自然に実装されます。モジュールをrequireすると、Luaはその結果をpackage.loadedにキャッシュします。その後のrequireの呼び出しでは、新しいテーブルではなく、同じキャッシュされたテーブルが返されます。
シングルトンモジュールの作成:
-- GameConfig.lua
local GameConfig = {
volume = 80,
difficulty = "normal"
}
function GameConfig:setVolume(v)
self.volume = v
end
return GameConfig
シングルトンの使用:
local config1 = require("GameConfig")
local config2 = require("GameConfig")
config1:setVolume(50)
print(config2.volume) -- 50 (同じテーブルです!)
config1とconfig2は両方とも、まったく同じテーブルを参照しています。一方の変数を通じて行われた変更は、もう一方の変数からも即座に確認でき、アプリケーション全体で共有された状態が保証されます。
シングルトンパターンはファクトリパターンとは異なります。ファクトリは新しいオブジェクトを作成するのに対し、シングルトンは一貫した状態を持つ単一の共有インスタンスを保証します。
自分で試してみよう
-- main.lua - Singletonパターンのデモンストレーション
-- 入力を読み込む
local newMusicVolume = tonumber(io.read())
local newDifficulty = io.read()
-- TODO: GameSettingsモジュールを変数(例: settings1)に読み込む
-- TODO: GameSettingsモジュールを別の変数(例: settings2)に再度読み込む
-- (Singletonが正しく機能していれば、両方の変数は同じテーブルを参照します!)
-- TODO: 最初の変数を使用して以下を行う:
-- - musicVolumeをnewMusicVolumeに設定する
-- - difficultyをnewDifficultyに設定する
-- TODO: 2番目の変数を使用して設定を表示する
-- 各項目を個別の行に表示: musicVolume, sfxVolume, difficulty
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。