なぜ大域データを使ってしまうのか?

ここ最近の調べことで非常に困ったことがあります。

ゲームエンジンでシーン遷移の際、前のシーンから次のシーンへデータを引き継ぎたいというニーズがあったのですが、

検索上位の対応方法がどれも「大域データに保存して、そのデータを読み込む」というやり方なのです。


例えば、Unityだと「staticな値で定義して読み込む」や「DontDestroyOnLoad()でインスタンスを残しておく」といった手法が検索上位に現れており、Godotだと公式ドキュメントが「スクリプトをシングルトンとして扱う」といった感じになります。

このように、シーン遷移だと大域データを用いるのが主流のようですが、私自身大域データの使用には否定的な意見です。 と言うのも、大域データには作った後のデメリットが多いからです。

今回は大域データにどんなデメリットがあるのかを考えていきたいと思います。

そして、どうしたら使わなくて済むかも考えていきたい

そもそも大域データって何?

これは私が愛読している達人プログラマーで初めてみた単語なのですが、アプリケーション中のすべてのメソッドがどこからでもアクセスできるデータのことを指すようです。

具体的にはグローバル変数、Singletonなどを指します。

なんで使われているの?

はっきりいって作るのがラクだからだと思います。

どこでもアクセスできる、と言うのは「そのデータはどこで使われるものなのか」を考える必要がないからです。

その代わり、それに変更するときに痛い目をみることになります。詳しくみていきましょう。

なんで大域データがいけないの?大域データのデメリット

すべてのメソッドから参照できる=結合度が非常に高い=あらゆるところに影響が出る

例えばfuncA()の中でグローバル変数Xを使っていたとしましょう。

def funcA():
    X = X + 123
    print(X)
    return X

このfuncAはグローバル変数Xを数値として考えています。

これを誰かがX = "HELLO"と変えたとしましょう。funcAが壊れることが明らかだと思います。

これがfuncAだけでなくB,C、はたまたhogehoge,fugafugaみたいな関数もXを使っていたらどうなりますか?


このようにグローバル変数は誰でも扱える変数なため、「誰がアクセスしてるか分かりません。」

そして、グローバル変数を変更すると、変更による影響が計り知れないものになります。

このような状態を結合度が高いと表現されることがありますが、この状態を引き起こすことが大域データを用いることの主要なデメリットになります。

ユニットテストが作りにくい

結合度が高いメソッドだとよくぶつかる問題なのですが、ユニットテストが非常に作りにくいです。

例えば、先ほどのfuncAをテストするtest_funcAを作ろうと思います。

# importとかはされていてXとfuncAを呼び出せる前提です

def test_funcA():
    global X
    X = 333
    result = funcA()
    assert result == 456 # false だとassertエラーになる。
    print("test_funcA passed.")

まず、テストでグローバル変数にアクセスできる必要があります。上ではさらっと流していますが、準備が結構めんどくさいことが多々あります。

そして、Xの値がわからないのでテスト内でXの値を変更する必要があります。

これで一応テストはパスできるとは思うのですが*1、Xを変更している以上、他のXを使っているテストやメソッドにも影響を与えています。

私がよく用いるテストツールは実際の実行とは別口で行うことが多いので実際のコードに影響を与えることが少ないと思われるのですが、もし常に起動しているソフトをテストしたい場合、上のようなテストコードを実行するとどうなるでしょう?

実コードにあるグローバル変数をいじることになるのでテストの値が残ったりと、想定外の影響を与えることになるだろうと言うのはイメージできるのではないかと思います。

大域データを使わないようにするには?

ここまでで、大域データのデメリットは大雑把ながらもイメージできたかと思います。

それではどうすれば使わなくていいか考えてみましょう。

引数として渡し、返り値で返す

先ほどのfuncAなどはXにあたるものを引数にすると、良いでしょう。

引数にすると変数の影響範囲が関数内のみになるため、値の管理がしやすくなります。

def funcA(x:int):
    x = x + 123
    print(x)
    return x


def test_funcA():
    x = 333
    result = funcA(x)
    assert result == 456
    print("test_funcA passed.")

こうすることでグローバル変数Xを456だろうがHELLOだろうがに変えてもfuncAもtest_funcAも影響を受けなくなります。 中で使っているのはプライベートな変数xだからです。

一時的にグローバルにするにしても内部用の変数に移し替える

次善の策としては、プライベートな変数にglobalな変数の値を格納して、その内部だけで取り扱うと言う考え方もいいのではないかと思います。

X = 333

def funcA(x:int):
    x = X + 123
    print(x)
    return x


def test_funcA():
    x = X
    result = funcA(x)
    assert result == 456
    print("test_funcA passed.")

上のようにして、取り扱うのをXでなくxのみにすれば、xを何か弄ろうとしてもグローバル変数の値が変わることがありません。*2


今回はここまでです。書いていたらなんかとっ散らかった内容になってしまった気がします。

次はもう少ししっかりまとめて投稿できればと思います。それではまた会いましょう。

*1:未検証です。

*2:pythonの場合、ミュータブルなlistとかdictだとアドレスを受け取っているため影響を与えることがあるので言語によっては元の変数に影響を与えることがあります。