OOPアプリケーションでのパラメーター管理


15

OOPの原則を実践する方法として、C ++で中規模のOOPアプリケーションを作成しています。

私のプロジェクトにはいくつかのクラスがあり、それらのいくつかはランタイム構成パラメーターにアクセスする必要があります。これらのパラメーターは、アプリケーションの起動時にいくつかのソースから読み取られます。ユーザーのhome-dirにある設定ファイルから読み込まれるものもあれば、コマンドライン引数(argv)であるものもあります。

そこで、クラスを作成しましたConfigBlock。このクラスは、すべてのパラメーターソースを読み取り、適切なデータ構造に格納します。例としては、構成ファイルまたは--verbose CLIフラグでユーザーが変更できるパスとファイル名があります。次に、ConfigBlock.GetVerboseLevel()この特定のパラメーターを読み取るために呼び出すことができます。

私の質問:このようなすべてのランタイム構成データを1つのクラスに収集することをお勧めしますか?

次に、クラスはこれらすべてのパラメーターにアクセスする必要があります。私はこれを達成するためのいくつかの方法を考えることができますが、どの方法を取るべきかはわかりません。クラスのコンストラクターは、次のようにConfigBlockへの参照を指定できます。

public:
    MyGreatClass(ConfigBlock &config);

または、CodingBlockの定義を含むヘッダー「CodingBlock.h」を含めるだけです。

extern CodingBlock MyCodingBlock;

その後、クラスの.cppファイルのみがConfigBlockのものを含めて使用する必要があります。
.hファイルは、このインターフェイスをクラスのユーザーに導入しません。ただし、ConfigBlockへのインターフェイスはまだありますが、.hファイルからは隠されています。

このように隠すのは良いですか?

インターフェイスをできるだけ小さくしたいのですが、最終的には、構成パラメーターを必要とするすべてのクラスがConfigBlockに接続する必要があると思います。しかし、この接続はどのように見えますか?

回答:


10

私は非常に実用主義者ですが、ここでの主な懸念は、これConfigBlockがインターフェイス設計をおそらく悪い方法で支配することを許可している可能性があることです。このようなものがある場合:

explicit MyGreatClass(const ConfigBlock& config);

...より適切なインターフェイスは次のようになります。

MyGreatClass(int foo, float bar, const string& baz);

...これらのfoo/bar/bazフィールドを大規模なものからチェリーピッキングするのとは対照的にConfigBlockです。

遅延インターフェースの設計

プラス面では、この種の設計により、コンストラクタ用の安定したインターフェイスを簡単に設計できます。たとえば、何か新しいものが必要になった場合、それをConfigBlock(おそらくコードを変更することなく)にロードして、インターフェースの変更は一切せずに、必要な新しいものを選択しますMyGreatClass。実装の変更のみです。

したがって、実際に必要な入力のみを受け入れる、より慎重に考え抜かれたインターフェイスを設計する自由が得られるのは、長所と短所の両方です。「これらの正確なパラメーターは、このインターフェースが機能するために必要なものです」というようなものとは対照的に、「この膨大なデータの塊を与えて、必要なものを選択します」という考え方を適用します

したがって、ここには間違いなくいくつかのプロがいますが、彼らは短所によってかなり重くされるかもしれません。

カップリング

このシナリオでは、ConfigBlockインスタンスから構築されているこのようなクラスはすべて、依存関係が次のようになります。

ここに画像の説明を入力してください

たとえば、Class2この図の単体テストを単体で行いたい場合、これはPITAになります。ConfigBlock関連するフィールドを含むさまざまな入力を表面的にシミュレートする必要がある場合がありますClass2条件でテストできるようにがあります。

あらゆる種類の新しいコンテキスト(ユニットテストまたは新しいプロジェクト全体)で、そのようなクラスは(再)使用の負担が大きくなる可能性がConfigBlockあります。それに応じて。

再利用性/展開性/テスト容易性

代わりに、これらのインターフェイスを適切に設計する場合、これらのインターフェイスを切り離して、次のようなものにすることができますConfigBlock

ここに画像の説明を入力してください

上の図で気づいた場合、すべてのクラスが独立します(それらの求心性/発信カップリングは1減少します)。

これにより、より多くの独立したクラスが得られます(少なくとも、 ConfigBlock)が作成され、新しいシナリオ/プロジェクトで(再)使用/テストするのがはるかに簡単になります。

現在、このClientコードは、すべてに依存し、すべてをまとめて組み立てる必要があるものになります。負担は最終的にこのクライアントコードに転送され、aから適切なフィールドを読み取り、ConfigBlockそれらをパラメーターとして適切なクラスに渡します。しかし、このようなクライアントコードは一般に特定のコンテキスト向けに厳密に設計されており、再利用の可能性は通常、とにかく閉じられます(アプリケーションのmainエントリポイント関数などが考えられます)。

したがって、再利用性とテストの観点から、これらのクラスをより独立させることができます。クラスを使用するインターフェイスの観点からはConfigBlock、すべてに必要なデータフィールド全体をモデル化する1つの大規模なモデルではなく、必要なパラメーターを明示的に指定することもできます。

結論

一般に、必要なものすべてを備えたモノリスに依存するこの種のクラス指向設計は、これらの種類の特性を持つ傾向があります。その結果、アプリケーションの適用可能性、展開可能性、再利用可能性、テスト可能性などが大幅に低下する可能性があります。それでも、私たちが積極的なスピンを試みれば、インターフェースの設計を多少簡素化できます。それらの長所と短所を測定し、トレードオフに価値があるかどうかを判断するのはあなた次第です。一般的に、より一般的で広く適用可能な設計をモデル化することを一般的に意図しているクラスのモノリスから選択するこの種の設計に対して誤りを犯す方がはるかに安全です。

最後だが大事なことは:

extern CodingBlock MyCodingBlock;

...これは、クラスをに結合するだけでなくConfigBlocks、その特定のインスタンスに直接結合するため、依存性注入アプローチよりも上記の特性の点で潜在的にさらに悪い(より歪んでいますか?)さらに、適用性/展開性/テスト容易性が低下します。

私の一般的なアドバイスは、少なくとも設計する最も一般的に適用可能なクラスについては、これらの種類のモノリスに依存せずにパラメーターを提供するインターフェイスの設計の側に間違いを犯すことです。そして、あなたが本当にそれを避けない非常に強力で自信のある理由がない限り、可能であれば、依存性注入なしのグローバルなアプローチを避けてください。


1

通常、アプリケーションの構成は主にファクトリオブジェクトによって消費されます。設定に依存するオブジェクトは、これらのファクトリオブジェクトの1つから生成する必要があります。Abstract Factoryパターンを利用して、ConfigBlockオブジェクト全体を取り込む1つのクラスを実装できます。このクラスは、他のファクトリオブジェクトを返すパブリックメソッドを公開ConfigBlockし、特定のファクトリオブジェクトに関連する部分のみを渡します。そうConfigBlockすれば、構成設定はオブジェクトからそのメンバーへ、そしてファクトリーファクトリーから工場へ「トリクルダウン」します。

言語をよく知っているのでC#を使用しますが、これはC ++に簡単に転送できるはずです。

public class ConfigBlock
{
    public ConfigBlock()
    {
        // Load config data and
        // connectionSettings = new ConnectionConfig();
        // connectionSettings...
    }

    private ConnectionConfig connectionSettings;

    public ConnectionConfig GetConnectionSettings()
    {
        return connectionSettings;
    }
}

public class FactoryProvider
{
    public FactoryProvider(ConfigBlock config)
    {
        this.config = config;
    }

    private ConfigBlock config;

    public ConnectionFactory GetConnectionFactory()
    {
        ConnectionConfig connectionSettings = config.GetConnectionSettings();

        return new ConnectionFactory(connectionSettings);
    }
}

public class ConnectionFactory
{
    public ConnectionFactory(ConnectionConfig settings)
    {
        this.settings = settings;
    }

    private ConnectionConfig settings;

    public Connection GetConnection()
    {
        return new Connection(settings.Hostname, settings.Port, settings.Username, settings.Password);
    }
}

その後、メインプロシージャでインスタンス化される「アプリケーション」として機能する何らかのクラスが必要になります。

// Your main procedure (yeah I'm bending the rules of C# a tad here,
// but you get the point).
int Main(string[] args)
{
    Application app = new Application();

    app.Run();
}

public class Application
{
    public Application()
    {
        config = new ConfigBlock();
        factoryProvider = new FactoryProvider(config);
    }

    private ConfigBlock config;
    private FactoryProvider factoryProvider;

    public void Run()
    {
        ConnectionFactory connections = factoryProvider.GetConnectionFactory();
        Connection connection = connections.GetConnection();

        connection.Connect();

        // Enter into your main loop and do what this program is meant to do
    }
}

最後の注意点として、これは.NETの「プロバイダーオブジェクト」として知られています。.NETのプロバイダーオブジェクトは、構成データをファクトリオブジェクトに結合するようです。これは、基本的にここで行いたいことです。

初心者向けプロバイダーパターンもご覧ください。繰り返しになりますが、これは.NET開発を対象としていますが、C#とC ++の両方がオブジェクト指向言語であるため、パターンは主に2つの間で転送可能です。

このパターンに関連するもう1つの優れた資料は、プロバイダーモデルです。

最後に、このパターンの批判:プロバイダーはパターンではありません


プロバイダーモデルへのリンクを除き、すべてが適切です。リフレクションはc ++ではサポートされていないため、機能しません。
BЈовић

@BЈовић:正しい。クラスリフレクションは存在しませんが、手動による回避策を組み込むことができます。これは基本的に、switchステートメントまたは構成ifファイルから読み取った値に対してテストするステートメントに委ねられます。
グレッグブルクハート

0

最初の質問:そのようなすべてのランタイム構成データを1つのクラスに収集するのは良い習慣ですか?

はい。実行時の定数と値、およびそれらを読み取るコードを一元化することをお勧めします。

クラスのコンストラクターには、ConfigBlockへの参照を指定できます

これは悪いことです。ほとんどのコンストラクターはほとんどの値を必要としません。代わりに、構築するのが簡単ではないすべてのインターフェイスを作成します。

古いコード(提案):

MyGreatClass(ConfigBlock &config);

新しいコード:

struct GreatClassData {/*...*/}; // initialization data for MyGreatClass
GreatClassData ConfigBlock::great_class_values();

MyGreatClassをインスタンス化します。

auto x = MyGreatClass{ current_config_block.great_class_values() };

ここに、クラスのcurrent_config_blockインスタンスConfigBlock(すべての値を含むもの)があり、MyGreatClassクラスはGreatClassDataインスタンスを受け取ります。つまり、コンストラクタに必要なデータのみを渡し、ConfigBlockそのデータを作成するための機能を追加します。

または、CodingBlockの定義を含むヘッダー「CodingBlock.h」を含めるだけです。

 extern CodingBlock MyCodingBlock;

その後、クラスの.cppファイルのみがConfigBlockのものを含めて使用する必要があります。.hファイルは、このインターフェイスをクラスのユーザーに導入しません。ただし、ConfigBlockへのインターフェイスはまだありますが、.hファイルからは隠されています。このように隠すのは良いですか?

このコードは、グローバルCodingBlockインスタンスがあることを示しています。それをしないでください:通常、アプリケーションが使用するエントリポイント(メイン関数、DllMainなど)でインスタンスをグローバルに宣言し、必要に応じて引数として渡す必要があります(ただし、上記で説明したように、渡さないでくださいクラス全体を囲み、データの周りのインターフェイスを公開し、それらを渡します)。

また、クライアントクラス(MyGreatClass)をのタイプに結び付けないでくださいCodingBlock。これはMyGreatClass、文字列と5つの整数を受け取る場合、を渡すよりも、その文字列と整数を渡すほうがよいことを意味しますCodingBlock


configからファクトリを分離することは良い考えだと思います。構成実装がコンポーネントをインスタンス化する方法を知っている必要はありません。これは必然的に、以前は一方向の依存関係しか存在していなかった二方向の依存関係になります。本当に問題のインターフェイスの共有ライブラリを使用する場合は特に、あなたのコードを拡張するとき、これは大きな意味を持っている
ジョエル・コルネット

0

短い答え:

あなたはしないでください、あなたのコード内のモジュール/クラスごとに、すべての設定を必要とします。そうした場合、オブジェクト指向設計に何か問題があります。特に、必要のないすべての変数を設定して単体テストを行う場合、そのオブジェクトを渡すことは、読み取りや保守には役立ちません。


この方法で、1つの中央の場所にパーサーコード(コマンドラインと構成ファイルを解析)を収集できます。その後、各クラスは関連するパラメーターをそこから選択します。あなたの意見では良いデザインは何ですか?
lugge86

たぶん私はそれを間違って書いたかもしれません-あなたはConfigBlockクラスであるかもしれない設定ファイル/環境変数から得られたすべての設定で一般的な抽象化を持っている(そしてその良い習慣)ことを意味します。ここでのポイントは、この場合、システム状態のコンテキスト、特にそうするために必要な値をすべて提供しないことです。
ダウィッドプラ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.