Featured image of post 第3回-複数の場所から設定を使おう — 設定ファイルマネージャーを作ってみよう

第3回-複数の場所から設定を使おう — 設定ファイルマネージャーを作ってみよう

複数のモジュールから設定を参照する際に発生する問題を体験。なぜ設定が反映されないのか、その原因を探ります。

@nqounetです。

前回は、外部の設定ファイルを読み込む機能を追加しました。

今回は、複数の場所から設定を使う方法を考えます。

今回のゴール

メインスクリプトと別のモジュールから、同じ設定を参照できるようにすることです。

ミキさんの新しい悩み

ミキさんのアプリが大きくなり、機能を別のモジュールに分割することになりました。

「ログ出力を担当するLoggerモジュールを作ったんだけど、そこからも設定(デバッグモードとか)を参照したいんだよね」

なるほど、それならLoggerモジュールでもConfigを使えばいいですね。

Loggerモジュールを作る

デバッグモードの設定を参照して、デバッグログを出力するかどうかを判断するLoggerモジュールを作ります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package Logger {
    use Moo;
    use v5.36;

    sub debug ($self, $message) {
        my $config = Config->new();
        $config->load_config('config.ini');

        if ($config->get('debug')) {
            say "[DEBUG] $message";
        }
    }
};

debugメソッドの中でConfignewして、設定ファイルを読み込んでいます。

メインスクリプトから使ってみる

メインスクリプトで設定を変更し、Loggerを使ってみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
use v5.36;
use Moo;

package Config {
    use Moo;

    has _settings => (is => 'ro', default => sub { {} });

    sub load_config ($self, $file) {
        open my $fh, '<', $file or die "Cannot open $file: $!";
        while (my $line = <$fh>) {
            chomp $line;
            next if $line =~ /^\s*$/;
            next if $line =~ /^\s*#/;

            if ($line =~ /^\s*(\w+)\s*=\s*(.+?)\s*$/) {
                my ($key, $value) = ($1, $2);
                $self->set($key, $value);
            }
        }
        close $fh;
    }

    sub set ($self, $key, $value) {
        $self->_settings->{$key} = $value;
    }

    sub get ($self, $key) {
        return $self->_settings->{$key};
    }
};

package Logger {
    use Moo;
    use v5.36;

    sub debug ($self, $message) {
        my $config = Config->new();
        $config->load_config('config.ini');

        if ($config->get('debug')) {
            say "[DEBUG] $message";
        }
    }
};

package main;

# メインスクリプトで設定を読み込む
my $config = Config->new();
$config->load_config('config.ini');

say "アプリ名: " . $config->get('app_name');

# ここでデバッグモードをOFFに変更
$config->set('debug', 0);

say "デバッグモード(メイン側): " . ($config->get('debug') ? 'ON' : 'OFF');

# Loggerを使ってデバッグログを出力
my $logger = Logger->new();
$logger->debug("処理を開始します");

config.iniは前回と同じものを使います。

1
2
3
4
# アプリケーション設定
app_name = MyApp
version = 1.0.0
debug = 1

実行してみよう

1
2
3
アプリ名: MyApp
デバッグモード(メイン側): OFF
[DEBUG] 処理を開始します

あれ?おかしいですね。

メイン側ではデバッグモードをOFFに設定したのに、Loggerはデバッグログを出力しています。

何が起きているのか?

図で見てみましょう。

	flowchart TB
    subgraph メインスクリプト
        A[Config->new] --> B[インスタンス1]
        B --> C[debug = 0 に設定]
    end

    subgraph Loggerモジュール
        D[Config->new] --> E[インスタンス2]
        E --> F[debug = 1 のまま]
    end

    B -.->|別々のインスタンス| E

問題は、メインスクリプトとLoggerで、それぞれ別のConfigインスタンスを作っていることです。

  • メインスクリプトのConfigインスタンス: debug = 0に変更
  • LoggerのConfigインスタンス: debug = 1のまま(設定ファイルから再読み込み)

newするたびに新しいインスタンスが作られるので、設定の変更が他の場所に反映されないのです。

ミキさんの反応

「え、設定を変更したのに反映されないって、これじゃ困る!どうすればいいの?」

確かに、これは困りますね。

ちなみに、今の実装ではdebug()を呼ぶたびに設定ファイルを読み込んでいるため、非常に非効率です。でも、これは次回で解決します。

考えられる解決策

いくつかの方法が考えられます。

方法1: インスタンスを引数で渡す

1
2
my $config = Config->new();
my $logger = Logger->new(config => $config);

動きますが、すべての場所で$configを渡し続ける必要があり、コードが煩雑になります。

方法2: グローバル変数を使う

1
our $CONFIG = Config->new();

シンプルですが、グローバル変数は予期しない副作用を起こしやすく、推奨されません。

方法3: ???

もっと良い方法があるはずです…。

第3回 完成コード

今回は「問題が発覚する」ことがゴールなので、問題のあるコードが完成コードです。

ファイル構成

1
2
3
.
├── app.pl
└── config.ini

app.pl

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
use v5.36;
use Moo;

package Config {
    use Moo;

    has _settings => (is => 'ro', default => sub { {} });

    sub load_config ($self, $file) {
        open my $fh, '<', $file or die "Cannot open $file: $!";
        while (my $line = <$fh>) {
            chomp $line;
            next if $line =~ /^\s*$/;
            next if $line =~ /^\s*#/;

            if ($line =~ /^\s*(\w+)\s*=\s*(.+?)\s*$/) {
                my ($key, $value) = ($1, $2);
                $self->set($key, $value);
            }
        }
        close $fh;
    }

    sub set ($self, $key, $value) {
        $self->_settings->{$key} = $value;
    }

    sub get ($self, $key) {
        return $self->_settings->{$key};
    }
};

package Logger {
    use Moo;
    use v5.36;

    sub debug ($self, $message) {
        my $config = Config->new();
        $config->load_config('config.ini');

        if ($config->get('debug')) {
            say "[DEBUG] $message";
        }
    }
};

package main;

my $config = Config->new();
$config->load_config('config.ini');

say "アプリ名: " . $config->get('app_name');

$config->set('debug', 0);

say "デバッグモード(メイン側): " . ($config->get('debug') ? 'ON' : 'OFF');

my $logger = Logger->new();
$logger->debug("処理を開始します");
# => デバッグログが出力されてしまう!(期待はOFFなのに)

config.ini

1
2
3
4
# アプリケーション設定
app_name = MyApp
version = 1.0.0
debug = 1

まとめ

  • 複数の場所から設定を使おうとして、問題が発覚した
  • newするたびに新しいインスタンスが作られる
  • 設定の変更が他の場所に反映されない
  • インスタンスを渡し続けるのは煩雑
  • グローバル変数は副作用が心配

次回予告

この問題を解決する方法を考えます。

「インスタンスを1つだけにできれば、どこからでも同じ設定が使えるんじゃない?」

次回は、クラス変数とinstance()メソッドを使って、インスタンスを1つに統一する方法を学びます。

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。