グローバル変数を使わずにグローバルな設定をする(Dart)
グローバル変数について
プログラミング言語における変数という機能は、私は slot(スロット) と抽象化して呼んでいます。これは、final や const 修飾子 を付けた 定数も変数と呼ぶことは混乱を招くからです。 slot は3種類に分けることができます。
- 変数(variable)
- 実行時定数(run time constant)
- コンパイル時定数(compile time constant)
dart で上記の slot を宣言する際、使われる修飾子はそれぞれ、var, final, const, です。 表題の、「グローバル変数を使わずに」とは、「どこからでもアクセス可能な var で宣言した slot(変数) を使わずに」という意味になります。
実際の code
今実際に書いている log class から一部抜粋します。 グローバルに設定したい内容は、yaml の インデント数、json か yaml どちらの形式で出力するか、などです。
class Log
{
static late final bool shortHand;
// immutatble なのかわからない外部 library の class は
// 適宜 スコープを private(「_」を付ける)にして不用意なアクセスによる
// 思いがけない変更がされないようにする。
static late final YAMLWriter _yamlWriter;
static late final bool isJsonFormat;
static void setFormat({
bool shortHand = true,
int indentSize = 4,
bool useJsonFormat = false,
}) {
// 名前の競合を避けるために 頭に Log と付けています。
// Log をつけてそろえることで、ぱっと見で、
// static slot に代入していることがわかりやすくなります。
Log.isJsonFormat = useJsonFormat;
Log.shortHand = shortHand;
Log._yamlWriter = YAMLWriter(indentSize: indentSize, allowUnquotedStrings: true);
}
String toYamlString() => 省略;
String toJsonString() => 省略;
@override
String toString() {
if (isJsonFormat) return toJsonString();
return toYamlString();
}
}
static late final
グローバルに設定したい項目を static late final で定義します。 * static late final bool shortHand; * static late final YAMLWriter _yamlWriter; * static late final bool isJsonFormat;
この3つです。 Log instance すべてで共通した値を参照したいので static を使います。 宣言時代入(slot を宣言したと同時に値を代入すること。造語です。)ではなく、後から代入したいので late を使います。 一度代入したら、そこから変更はされないようにしたいので final を使います。
setFormat
static な setFormat method を使って、上記で定義した slot たちに値を設定する処理を行います。今回は Log の形式を決めるので setFormat という名前になっています。
toString
toString の部分で isJsonFormat を参照し、yaml で返すか json で返すかの分岐をしています。
実際に使う
void main() {
Log.setFormat(
shortHand: false,
indentSize: 4,
useJsonFormat: false,
);
final logA = Log(); // Log instance の生成
final logB = Log();
print(logA);
print(logB);
}
処理の開始に Log.setFormat を一度だけ呼ぶ
Log instance を生成する前に Log.setFormat を一度だけ呼ぶ必要があります。 実行時に static late final で定義した slot に値が代入されていないのに、これらを参照する処理が発生すると エラーになります。 なので、参照する前に値を代入する必要があります。 また、一度 setFormat を呼んだ後に、さらにsetFormat を呼び、再代入しようとすると、エラーになります。 * 値が代入される前に参照しようとするとエラー * 再代入しようとするとエラー
なので、処理の一番初めに Log.setFormat を一度だけ呼びましょう。
おわりに
より堅牢な書き方をするなら、static late final で宣言した slot のスコープを private にした方がよさそうです。 複数人での開発や、ライブラリとして公開するといった場合など。
static late final bool _shortHand;
static late final YAMLWriter _yamlWriter;
static late final bool _isJsonFormat;
今回は、自分の一人の開発で、setFormat を呼ぶ以外の方法で代入など、するわけないのでやってませんが。
private にした slot を外部から参照しようと思った場合 別途 getter を定義しなければならないので、その分、手数が増えます。
これらは全部 private にして getter も定義せず Log instance 以外からのアクセスが無い(Log class 以外からの依存が無い)という状態を保証する方が良さげですね。
つまり、この設定を変更しても、直接的に影響が及ぶのは、Log class だけということを保証します。
そういえば、そもそも論として、なぜ、グローバル変数が推奨されないのかというと。 一言でいうなら、人間の脳の認知能力の限界、とでもいえばいいのでしょうか。 人は何か巨大な物事を管理しようとしたとき、それぞれに単位をつけたりして、グループ分けして、小さなものの集合体として、管理します。グローバル変数はその定石をぶち破り、真逆をいく存在だからです。 例えば、バグが発生し、その原因を追究するときに、小さく分けていれば、原因の当てをつけやすくなり、調べる範囲も小さくなります。 グローバル変数が存在し、それによる影響範囲がどこまであるのか定かでない場合、当てがわからず、調べる範囲が全体になってしまったりします。 規模が大きくなればなるほど、悪影響は甚大になります。