これは実際には非常に重要な質問であり、ほとんどすべてのアプリケーションのコア部分であるにもかかわらず、十分に重要視されていないため、しばしば間違って行われます。これが私のガイドラインです:
すべての設定を含む構成クラスは、単純な古いデータ型struct / classでなければなりません。
class Config {
int prop1;
float prop2;
SubConfig subConfig;
}
メソッドを持つ必要はなく、継承を伴うべきではありません(バリアントフィールドを実装するための言語での唯一の選択肢でない限り、次の段落を参照してください)。構成を使用して、設定をより小さな特定の構成クラス(上記のsubConfigなど)にグループ化できます。この方法で行う場合は、依存関係が最小限であるため、単体テストおよびアプリケーション全般で合格することが理想的です。
異なるセットアップの構成が異質である場合、バリアントタイプを使用する必要があるでしょう。値を読み取って正しい(サブ)構成クラスにキャストするときに、動的キャストを配置する必要があることは認められています。これは、別の構成設定に依存していることは間違いありません。
これを行うだけで、すべての設定をフィールドとして入力するのを怠ってはいけません。
class Config {
Dictionary<string, string> values;
};
これは、どのフィールドを扱っているかを知る必要のない一般化されたシリアル化クラスを記述できることを意味するので魅力的ですが、それは間違っているので、その理由をすぐに説明します。
構成のシリアル化は、完全に別のクラスで行われます。これを行うために使用するAPIまたはライブラリが何であれ、シリアル化関数の本体には、基本的にはファイル内のパス/キーからオブジェクトのフィールドへのマップとなるエントリが含まれている必要があります。優れたイントロスペクションを提供し、すぐにこれを実行できる言語もあれば、明示的にマッピングを作成する必要がある言語もありますが、重要なことは、マッピングを一度だけ作成すればよいことです。たとえば、私がc ++ boostプログラムオプションパーサーのドキュメントから採用したこの抜粋を検討してください。
struct Config {
int opt;
} conf;
po::options_description desc("Allowed options");
desc.add_options()
("optimization", po::value<int>(&conf.opt)->default_value(10);
最後の行は基本的に「最適化」がConfig :: optにマップすることを示しており、期待する型の宣言があることにも注意してください。タイプが予期したものではない場合、ファイル内のパラメーターが実際にはfloatまたはintでない場合、または存在しない場合、構成の読み取りを失敗させたいと考えています。つまり、ファイルのフォーマット/検証に問題があるため、ファイルを読み取るときに障害が発生し、except / returnコードをスローして正確な問題を報告する必要があります。これをプログラムの後半に遅らせることはできません。そのため、上記のように、すべてのディクショナリスタイルのConfをキャッチするように誘惑されるべきではありません。これは、ファイルが読み取られても失敗しない-値が必要になるまでキャストが遅延されるためです。
何らかの方法でConfigクラスを読み取り専用にする必要があります。クラスを作成してファイルから初期化するときに、クラスのコンテンツを一度設定します。アプリケーションで動的設定を変更する必要がある場合、および変更しないconst設定を使用する必要がある場合は、configクラスのビットを読み取り専用にしないで、動的設定を処理する別のクラスを用意する必要があります。 。
理想的には、プログラムの1つの場所でファイルを読み取る、つまり「ConfigReader
」のインスタンスが1つしかない場合。ただし、Configインスタンスを必要な場所に渡すのに苦労している場合は、グローバル構成を導入するよりも2つ目のConfigReaderを用意することをお勧めします(これは、OPが「静的")、それは私を私の次のポイントに連れて行きます:
シングルトンの魅惑的なサイレンの歌は避けてください。「そのクラスクラスを渡す必要がなくなるので、すべてのコンストラクターが美しく、すっきりします。続ければ、とても簡単になります。」真実とは、Configクラスまたはその一部をアプリケーションの多くのクラスに渡す必要がほとんどない、適切に設計されたテスト可能なアーキテクチャです。あなたが見つけるもの、あなたのトップレベルのクラス、あなたのmain()関数、または何でも、あなたはそれらを一緒に戻す引数としてコンポーネントクラスに提供するであろう個々の値にconfを解明します(手動の依存関係)注入)。シングルトン/グローバル/静的confを使用すると、アプリケーションのユニットテストを実装および理解することがはるかに困難になります。たとえば、テストするためにグローバル状態を設定する必要があることを知らないチームの新規開発者を混乱させます。
言語がプロパティをサポートしている場合は、この目的でそれらを使用する必要があります。その理由は、1つ以上の他の設定に依存する「派生」構成設定を非常に簡単に追加できることを意味します。例えば
int Prop1 { get; }
int Prop2 { get; }
int Prop3 { get { return Prop1*Prop2; }
言語がプロパティイディオムをネイティブでサポートしていない場合は、同じ効果を達成するための回避策があるか、ボーナス設定を提供するラッパークラスを作成するだけです。プロパティの利点を他の方法で提供できない場合は、手動で記述し、単にオブジェクト指向の神を喜ばせる目的でゲッター/セッターを使用するのは時間の無駄です。あなたは平野の古いフィールドでより良くなるでしょう。
優先順位に従って、異なる場所から複数の構成をマージおよび取得するシステムが必要になる場合があります。この優先順位は明確に定義され、すべての開発者/ユーザーが理解できる必要があります。たとえば、WindowsレジストリHKEY_CURRENT_USER / HKEY_LOCAL_MACHINEを検討してください。構成を読み取り専用に保つことができるように、この機能的なスタイルを実行する必要があります。つまり、
final_conf = merge(user_conf, machine_conf)
のではなく:
conf.update(user_conf)
もちろん、選択したフレームワーク/言語が独自の組み込みのよく知られている構成メカニズムを提供する場合は、もちろん独自にロールするのではなく、それを使用する利点を検討する必要があります。
そう。考慮すべき多くの側面-正しく理解すると、アプリケーションアーキテクチャに大きな影響を与え、バグを減らし、テストが容易になり、他の場所で優れたデザインを使用するように強制します。