グローバルスコープは要らない?

このトピックで扱うのは、グローバルスコープの「変数」よ。
グローバルスコープの「関数」は、要らないはずがないですもんね。
そういうこと!

グローバルスコープの変数は、「他のモジュールからもアクセスできるようにしたい場合」に使います。

例えば次のプログラムを見てみましょう。1つ前のトピックとよく似たC言語のプログラムです。

document.h
#ifndef document_h
#define document_h

typedef enum {
  Closed,
  Open
} DocumentOpenStatus;

extern DocumentOpenStatus openStatus;

void OpenDocument(const char *pDocumentName);
void CloseDocument(void);

#endif /* document_h */
document.c
#include <assert.h>
#include "document.h"

DocumentOpenStatus openStatus = Closed;

void OpenDocument(const char *pDocumentName) {
  assert(openStatus == Closed);

  /* ドキュメントを開く処理(省略) */

  openStatus = Open;
}

void CloseDocument(void) {
  assert(openStatus == Open);

  /* ドキュメントを閉じる処理(省略) */

  openStatus = Closed;
}

1つ前のトピックのプログラムから変わったのは、ドキュメントが開いているかどうかを示す変数openStatusがグローバルになったことです。

「document.c」からstaticを削除して、ヘッダーファイル「document.h」にextern宣言を追加してあります。それにともなって、変数の型(enum)の宣言を「document.c」から「document.h」に移動しました。

これで何ができるようになるかは、main()を見れば分かるでしょう。

main.c
#include <stdlib.h>
#include "document.h"

int main(void) {
  OpenDocument("document-name");

  if (openStatus == Open) {
    CloseDocument();
  }

  return EXIT_SUCCESS;
}

グローバルになった変数openStatusmain()からアクセスして、ドキュメントが開いているかどうかを判定できるようになったのです。

……あれ?ちょっと待ってください!そもそも変数をファイルスコープにしていたのには、理由がありましたよね。
ふふふ。正解!変数の値が予期せぬタイミングで変更されないように、ファイルスコープにしていたのよね。

グローバルにしてしまったら、変数の値が勝手に変更されないことを誰が保証してくれるのでしょう?もちろん、「この変数は更新しない」というルールを守ってコーディングできれば問題ありませんが、それはなかなか難しいことです。

プログラムの規模が大きくなるにつれグローバル変数の数が増えれば、ややこしいルールもどんどん増えていくでしょう。そして、いつか手におえないほど複雑になってしまうかもしれません。(実際に、そのようなプログラムで苦労したことがある人もいるのでは?)

つまり、ファイルスコープだった変数をグローバルに変更するのは、「情報隠蔽」が破られてしまうことを意味するのよ。
じゃあ、この方法はあまり良いやり方ではないんですね……
そういうこと。

ここからは、「情報隠蔽」を保ったまま(同時に気を配らなければならない範囲を狭くしたまま)、やりたいことができる方法を考えてみましょう。グローバル変数を使わずに、ドキュメントが開いているかどうかを判定できるようにするのです。

次の改良版プログラムを見てください。

document.h
#ifndef document_h
#define document_h

typedef enum {
  Closed,
  Open
} DocumentOpenStatus;

DocumentOpenStatus GetOpenStatus(void);

void OpenDocument(const char *pDocumentName);
void CloseDocument(void);

#endif /* document_h */
document.c
#include <assert.h>
#include "document.h"

static DocumentOpenStatus openStatus = Closed;

DocumentOpenStatus GetOpenStatus(void) {
  return openStatus;
}

void OpenDocument(const char *pDocumentName) {
  assert(openStatus == Closed);

  /* ドキュメントを開く処理(省略) */

  openStatus = Open;
}

void CloseDocument(void) {
  assert(openStatus == Open);

  /* ドキュメントを閉じる処理(省略) */

  openStatus = Closed;
}

変数openStatusは、staticを付けてファイルスコープに戻しました。その代わり、変数の値を返す関数GetOpenStatus()が追加されたのが分かるでしょうか。

main()がどうなるのかも、見てみましょう。

main.c
#include <stdlib.h>
#include "document.h"

int main(void) {
  OpenDocument("document-name");

  if (GetOpenStatus() == Open) {
    CloseDocument();
  }

  return EXIT_SUCCESS;
}

追加されたGetOpenStatus()により、ドキュメントが開いた状態かどうかを判定できています。一方、もうopenStatusに直接アクセスすることはできません。

グローバル関数を介してあげることで、ファイルスコープ変数の「情報隠蔽」を保っているんですね!
そう。このようなアプローチをとれば、グローバル変数はほとんど必要なくなるわね。

今回の例のように、「ファイルスコープ変数の値を取り出す関数」を用意する方法には、もうひとつ重要な効果があります。それは、「インターフェースと実装の分離」です。

GetOpenStatus()関数は、インターフェースとして公開されています。でも、その内部でopenStatusにアクセスしていることまでは公開していません。つまり、実装を隠しています。

そのため、インターフェースを変えることなく、あとから実装だけを変更できるのです!

例えば、ドキュメントが開いているかどうかをファイルポインタで判定するように変えたくなるかもしれません。そのために変数を差し替えても、モジュールの外からみたGetOpenStatus()の振る舞いは変えずに済むでしょう。

static FILE *documentFile = 0;

DocumentOpenStatus GetOpenStatus(void) {
  return (documentFile)? Open : Closed;
}

もしグローバル変数を使っていたら、こうした変更は簡単ではなくなります。

なるほど。グローバル変数は、極力使わない方がメンテナンスも楽になりそうですね!
そういうこと。一方で、変数に直接アクセスせず関数を介す場合は、関数呼び出しのオーバーヘッドがかかるってことも一応覚えておいて。

関数呼び出しのオーバーヘッドは、ほとんどの場合は気にする必要がないほど小さなものです。それでも気になるようなら、inline関数を使うようにしましょう。ただし、コンパイラがinline関数をサポートしていないケースもあります。

どうしても高速化が必要な場合は、涙をのんでグローバル変数にすることもあるわ。
あははは。グローバル変数は、どうしても使わなければならない理由があるときだけ、ですね。
ここがポイント!
情報隠蔽の観点から、グローバルスコープの変数は極力使用しないようにしましょう。代わりに、インターフェースだけ公開して実装との分離を図れば、メンテナンス性も向上します。