首页 > 解决方案 > More concise way to build a configuration class using environment variables?

问题描述

I have a class Configuration that reads in environment variables:

class Configuration {
    has $.config_string_a;
    has $.config_string_b;
    has Bool $.config_flag_c;

    method new() {
        sub assertHasEnv(Str $envVar) {
            die "environment variable $envVar must exist" unless %*ENV{$envVar}:exists;
        }

        assertHasEnv('CONFIG_STRING_A');
        assertHasEnv('CONFIG_STRING_B');
        assertHasEnv('CONFIG_FLAG_C');

        return self.bless(
            config_string_a => %*ENV{'CONFIG_STRING_A'},
            config_string_b => %*ENV{'CONFIG_STRING_B'},
            config_flag_c => Bool(%*ENV{'CONFIG_FLAG_C'}),
        );
    }
}

my $config = Configuration.new;

say $config.config_string_a;
say $config.config_string_b;
say $config.config_flag_c;

Is there a more concise way to express this? For example, I am repeating the environment variable name in the check and the return value of the constructor.

I could easily see writing another, more generic class that encapsulates the necessary info for a config parameter:

class ConfigurationParameter {
    has $.name;
    has $.envVarName;
    has Bool $.required;

    method new (:$name, :$envVarName, :$required = True) {
        return self.bless(:$name, :$envVarName, :$required);
    }
}

Then rolling these into a List in the Configuration class. However, I don't know how to refactor the constructor in Configuration to accommodate this.

标签: raku

解决方案


想到的最直接的变化是改变new为:

method new() {
    sub env(Str $envVar) {
        %*ENV{$envVar} // die "environment variable $envVar must exist"
    }

    return self.bless(
        config_string_a => env('CONFIG_STRING_A'),
        config_string_b => env('CONFIG_STRING_B'),
        config_flag_c => Bool(env('CONFIG_FLAG_C')),
    );
}

虽然//是定义性检查而不是存在性检查,但未定义环境变量的唯一方法是未设置它。这归结为%*ENV每个环境变量的提及和提及。

如果只有几个,那么我可能会停在那里,但让我印象深刻的下一点重复是属性的名称只是环境变量名称的小写,所以我们也可以消除这种重复,在稍微复杂一点的成本:

method new() {
    multi env(Str $envVar) {
        $envVar.lc => %*ENV{$envVar} // die "environment variable $envVar must exist"
    }
    multi env(Str $envVar, $type) {
        .key => $type(.value) given env($envVar)
    }

    return self.bless(
        |env('CONFIG_STRING_A'),
        |env('CONFIG_STRING_B'),
        |env('CONFIG_FLAG_C', Bool),
    );
}

现在env返回 a Pair,并将|其展平到参数列表中,就好像它是一个命名参数一样。

最后,“电动工具”的方法是在类之外写一个这样的特征:

multi trait_mod:<is>(Attribute $attr, :$from-env!) {
    my $env-name = $attr.name.substr(2).uc;
    $attr.set_build(-> | {
        with %*ENV{$env-name} -> $value {
            Any ~~ $attr.type ?? $value !! $attr.type()($value)
        }
        else {
            die "environment variable $env-name must exist"
        }
    });
}

然后将类写为:

class Configuration {
    has $.config_string_a is from-env;
    has $.config_string_b is from-env;
    has Bool $.config_flag_c is from-env;
}

特征在编译时运行,并且可以以各种方式操作声明。此特征根据属性名称计算环境变量的名称(属性名称总是像$!config_string_a,因此是substr)。set_build设置将在创建类时运行以初始化属性的代码。这会传递各种在我们的情况下并不重要的东西,所以我们忽略|. with就像 一样,所以这if defined与前面的方法相同//。最后,Any ~~ $attr.type检查询问参数是否以某种方式受到约束,如果是,则执行强制(通过调用具有值的类型来完成)。


推荐阅读