設定情報をエレガントに管理する方法(解決なし)

アプリケーションにはいろいろ設定があるものです。一般的な例では、production / development のようなモードや、ログレベルなど。プログラムを書くとき、こういうデータをどのように管理しますか?

大きく 2 種類の書き方があると思います。

  1. グローバル変数(やそれに類するもの)に入れる
  2. クラス生成時にコンストラクタに引き回す

どちらも一長一短があると思っています。

1. グローバル変数に入れる

たとえば、$config = { mode: :development } などとするだけ。

Rubyの場合、グローバル変数だけでなく、定数に入れたり、クラスメソッドにしたり、singletonライブラリを使ったり、スレッドローカルストレージを使ったりするのもこの分類に含めます。

長所

  • 記述がとても簡潔
  • 設定の追加が簡単

短所

  • グローバル変数を使うことに罪悪感がある
  • 1 つのプロセスで唯一の設定しか持てない

解説

とにかく簡単な方法。設定情報が必要なときはグローバル変数を読み出すだけなので、プログラムの記述は非常に簡潔。また、設定を追加したくなったときも自明。名前の衝突が気になるなら、$MyApp_configのように、アプリケーション名をprefixすると良さそう(ただし長くなるので、記述性は若干下がる)。

この方法を選びたくない最大の理由は、気持ちの問題だと思う。教育されたプログラマは「グローバル変数はダメ」と心に刷り込まれている。しかし設定情報をグローバル変数に入れることは、そこまで問題ではない気もする。グローバル変数がダメなのは、実行中にグローバル変数を書き換えることでモジュール間に暗黙的な依存が発生してしまうためだけれど、設定情報はアプリ初期化時以外に代入されることはないはずなので、意図しない依存は発生しないはず。

この方法の実用上の問題は、設定情報をプロセス内で1つしか持てないこと。つまり、同一プロセス内で異なる設定のアプリケーションインスタンスを複数作ることができない。大して問題ないと思うかもしれないが、テスト時などに様々な設定でのアプリケーションインスタンスを作りたくなったり、アプリケーションをライブラリとして使いたくなったりしたときに困る。

なお、定数やクラスメソッドなども本質的にはグローバル変数と変わらないので、プロセスの問題は解決しない *1 。スレッドローカルストレージを使っても、本質的な問題は変わらない *2

2. クラス生成時にコンストラクタに引き回す

たとえば次のように、クラスごとにインスタンス変数@configなどで保持する。設定情報が必要なクラスには必ず設定情報を渡す。

class MyApp::Foo
  def initialize(config, ...)
    @config = config
    ...
    @bar = Bar.new(@config, ...)
    ...
  end
end

長所

短所

  • 記述が極めて冗長(ほとんどすべてのクラスに設定情報をたらい回しする必要がある)
  • インスタンスを大量に生成する場合、速度やメモリ使用量がやや気になる(多くの場合は無視できるとは思う)

解説

教科書的に正しそうな方法。グローバル変数の類が一切ないので罪悪感がない。また、異なる設定のアプリケーションインスタンスを作ることもできる。

しかし、記述性は明らかにグローバル変数の方法より劣る。initializeは必ず@configを受け取る必要があり、newには必ず@configを渡す必要がある。

また、ASTのノードのクラスのように、インスタンスが大量生成される可能性があるクラスに毎回@configをもたせるのは、無駄が気にならなくもない。ほとんどの場合、すべての@configには同じオブジェクトへの参照が入るだけなので。(ただし、実際に問題になる可能性は高くないと思う)

近い将来にアプリケーションインスタンスを複数作る必要がない場合、この方法を選ぶメリットはないと思う。しかし、必要になってから書き換えるには、変更量は非常に多くてつらい。しかし、だからといって防衛的にこっちの方法を選んで記述性や実行効率を下げるのは、生産性を下げていると感じてしまう……。

なお、この方法のバリアントとして、「すべての設定情報を渡すのではなく、そのクラスが必要としている情報だけを渡す」というのがある。これはより「正しい」感じがするが、生産性はより最悪になる *3

どうなれば幸せか

上の 2 つはどちらを選んでも何か間違っているような気分になり、いつもモヤモヤしています。なにか言語機能が不足しているのではないかと思うのですが、どういう機能があれば綺麗に解決するのかもよくわかりません。

  • 動的スコープな変数があればよいのでは?と思っていた時期もあったのですが、複数のアプリケーションインスタンスを相互に実行するような場合にはイマイチ使えなさそう。
  • オブジェクトをグループ化する機能があればよさそうな気がしますが、記述性を下げずに所属グループを指定する方法を思いつかない。Ractor が近そうだけれど、Ractor はやはり別物なので多分転用はむずかしい(シェアされたオブジェクトの中で設定を参照すると曖昧とか)。

まとまらないまとめ

わかりやすそうなので「設定情報」で話しましたが、アプリケーション全体の管理を行う(半グローバルな)インスタンスも同様にすべての関係インスタンスに参照もたせたい気持ちになります。

この問題をエレガントに解決している言語の事例とか言語機能の論文とかあれば、教えてください。

*1:グローバル変数」という言葉は避けられるので、気持ちは落ち着くかもしれない。実際、実行中にうっかり書き換えてしまうリスクは減るので、多少改善はしてそう。

*2:「スレッド内で 1 つ」に緩和できるものの、アプリケーションインスタンスを複数同居させられない問題自体は変わらない。あと記述も長くなりがち。

*3:「クラスが必要としている情報」はなかなか予測できないし、そのクラス自身が必要としなくても、そのクラスの中で別のクラスをインスタンス化する際に必要になるということもありうる。