私は中程度のサイズOOPアプリケーションをC++で実践する方法として書いていますOOP原則。
私のプロジェクトにはいくつかのクラスがあり、そのうちのいくつかはランタイム構成パラメーターにアクセスする必要があります。これらのパラメータは、アプリケーションの起動時にいくつかのソースから読み取られます。一部はユーザーのホームディレクトリの設定ファイルから読み込まれ、一部はコマンドライン引数(argv)です。
そこで、クラスConfigBlock
を作成しました。このクラスは、すべてのパラメーターソースを読み取り、適切なデータ構造に格納します。例としては、設定ファイルでユーザーが変更できるパスとファイル名、または--verbose CLIフラグがあります。次に、この特定のパラメータを読み取るためにConfigBlock.GetVerboseLevel()
を呼び出すことができます。
私の質問:そのようなすべてのランタイム構成データを1つのクラスに収集することは良い習慣ですか?
次に、私のクラスはこれらすべてのパラメーターにアクセスする必要があります。これを実現する方法はいくつか考えられますが、どれを採用すればよいかわかりません。クラスのコンストラクタは、次のようにConfigBlockへの参照を指定できます
public:
MyGreatClass(ConfigBlock &config);
または、自分のCodingBlockの定義を含むヘッダー「CodingBlock.h」を含めるだけです。
extern CodingBlock MyCodingBlock;
次に、クラスの.cppファイルのみがConfigBlockのものを含めて使用する必要があります。
。hファイルは、クラスのユーザーにこのインターフェースを導入しません。ただし、ConfigBlockへのインターフェイスはまだ残っていますが、.hファイルからは隠されています。
このように非表示にしてもいいですか?
インターフェイスをできるだけ小さくしたいのですが、結局のところ、構成パラメーターを必要とするすべてのクラスは、ConfigBlockに接続する必要があると思います。しかし、この接続はどのように見えるべきですか?
私はかなり実用主義者ですが、ここでの主な関心事は、このConfigBlock
がインターフェース設計をおそらく悪い方法で支配することを許可しているのではないかということです。あなたがこのようなものを持っているとき:
explicit MyGreatClass(const ConfigBlock& config);
...より適切なインターフェースは次のようになります。
MyGreatClass(int foo, float bar, const string& baz);
...大規模なConfigBlock
からこれらのfoo/bar/baz
フィールドを選択するのではなく、.
レイジーインターフェイスデザイン
プラス面では、この種の設計により、コンストラクターの安定したインターフェイスを簡単に設計できます。たとえば、新しいものが必要になった場合は、それをConfigBlock
(おそらくコードを変更せずに)にロードしてからチェリーを実行するだけです-インターフェイスの種類を変更せずに、必要な新しいものをすべて選択します。MyGreatClass
の実装の変更のみです。
したがって、実際に必要な入力のみを受け入れる、より注意深く考え抜かれたインターフェースを設計する必要がなくなるのは、賛否両論です。 "この大量のデータの塊を私に与えてください、私はそれから必要なものを選びます"のようなものとは対照的に、-"これらの正確なパラメータはこのインターフェースが機能するために必要なもの。」
したがって、ここには確かにいくつかの長所がありますが、短所の方がはるかに多いかもしれません。
カップリング
このシナリオでは、ConfigBlock
インスタンスから構築されているそのようなクラスはすべて、依存関係が次のようになります。
たとえば、この図のClass2
を単体で単体テストする場合、これはPITAになる可能性があります。さまざまな条件下でテストできるように、Class2
が関心を持っている関連フィールドを含むさまざまなConfigBlock
入力を表面的にシミュレートする必要がある場合があります。
あらゆる種類の新しいコンテキスト(ユニットテストであれ、まったく新しいプロジェクトであれ)で、そのようなクラスは(再)使用の負担が大きくなる可能性があります。ライドのために常にConfigBlock
を持ち込み、設定する必要があるためです。それに応じて。
再利用性/展開性/テスト容易性
代わりに、これらのインターフェースを適切に設計すると、それらをConfigBlock
から切り離して、次のようなものにすることができます。
この上の図で気付いた場合、すべてのクラスが独立しています(求心性/発信結合が1つ減少します)。
これにより、より多くの独立したクラス(少なくともConfigBlock
とは独立)が生まれ、新しいシナリオ/プロジェクトでの(再)使用/テストがはるかに簡単になります。
現在、このClient
コードは、すべてに依存し、すべてをまとめる必要があるコードになっています。負担は最終的にこのクライアントコードに転送され、ConfigBlock
から適切なフィールドを読み取り、それらをパラメーターとして適切なクラスに渡します。しかし、そのようなクライアントコードは一般に特定のコンテキスト向けに狭く設計されており、再利用の可能性は通常、いずれにせよ完全に近いか、またはそれに近いものになります(アプリケーションのmain
エントリポイント関数などの可能性があります)。
したがって、再利用性とテストの観点から、これらのクラスをより独立させるのに役立ちます。クラスを使用するユーザーのインターフェースの観点からは、すべてに必要なデータフィールドの全体をモデル化する1つの巨大なConfigBlock
ではなく、必要なパラメーターを明示的に示すのに役立ちます。
結論
一般に、必要なすべてを備えたモノリスに依存するこの種のクラス指向設計は、これらの種類の特性を持つ傾向があります。その結果、適用性、展開性、再利用性、テスト性などが大幅に低下する可能性があります。それでも、ポジティブスピンを試みた場合、インターフェイスの設計を単純化できます。これらの長所と短所を測定し、トレードオフに価値があるかどうかを判断するのはあなた次第です。通常、より一般的で広く適用可能なデザインをモデル化することを目的としたクラスのモノリスからチェリーピックするこの種のデザインに反する方がはるかに安全です。
最後だが大事なことは:
extern CodingBlock MyCodingBlock;
...これは、依存関係注入アプローチよりも、上記の特性の点でクラスをConfigBlocks
だけでなく特定のインスタンスに直接結合することになるため、潜在的にさらに悪い(歪んでいるか?) =の。これにより、適用性/展開性/テスト性がさらに低下します。
私の一般的なアドバイスは、これらの種類のモノリスに依存せずにパラメーターを提供するインターフェースを設計する側に誤りを犯すことです。そして、できる限り、依存性注入なしのグローバルなアプローチは避けてください。回避しないという非常に強力で自信のある理由がない限り、そうしないでください。
通常、アプリケーションの構成は主にファクトリオブジェクトによって使用されます。構成に依存するオブジェクトは、これらのファクトリオブジェクトの1つから生成する必要があります。 Abstract Factory Pattern を利用して、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つの優れた資料 プロバイダーモデル 。
最後に、このパターンの批評: プロバイダーはパターンではありません
最初の質問:そのようなすべてのランタイム構成データを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
を渡すよりも、その文字列と整数を渡すほうが得策です。
あなたはしないでくださいコード内の各モジュール/クラスのすべての設定が必要です。もしそうなら、オブジェクト指向のデザインに何か問題があります。特にユニットテストの場合、不要なすべての変数を設定し、そのオブジェクトを渡すことは、読み取りや維持に役立ちません。