web-dev-qa-db-ja.com

インターフェイスを実装する複数の具象クラスに関する一般的なアーキテクチャの問題を解決するための助けが必要

過去にFactoryクラスで解決した共通の問題が発生しましたが、常に少し「オフ」に感じました。

単一のメソッドを持つIExporterを実装する複数のExporterクラスがあるとします。

void Export(string outputPath);

...各Exporterクラスは、さまざまなタイプのデータ(たとえば、データベースまたは配列などのデータ)をエクスポートする方法を認識しています。ただし、このため、各エクスポータークラスには、この情報を注入する必要があります。私は具象クラスのコンストラクタを使用してこれに取り組みます:

DatabaseExporter(DbContext dbContext, string tableName) {...}
ArrayExporter(string[] arrayData) {...}

状況に応じて、DatabaseExporterまたはArrayExporterのいずれかをインスタンス化する必要があります。しかし、これらのクラスの1つをその場でインスタンス化すると、コードのテスト容易性が損なわれます(つまり、モックエクスポーターを使用できません)。また、IExporterのパラメーター(DbContextまたは配列)を何らかの方法で入力する必要があるため、IExporterを挿入することはできません。

したがって、私が常に最終的に解決するのは、さまざまなタイプのエクスポーターをそれぞれ作成するためのメソッドを持つ、注入可能なExporterFactoryを作成することです。例えば:

IExporter CreateDatabaseExporter(DbContext dbContext, string tableName);
IExporter CreateArrayExporter(string[] arrayData);

これはこの問題への健全なアプローチですか? 1つのクラスにさまざまな構築実装をすべて含めるのは奇妙に感じられますが、それがファクトリーのポイントです。あるいは、パターンを誤解しているのかもしれません。誰かがこれを片付けるのを手伝ってくれませんか?

6
vargonian

問題を分解する

すべてのエクスポーターが同じように機能するであることが確認されました。基本的な例(これについては後で説明します)として、これは次のようなものです。

_IExporter exporter = GetWhateverExporterYouWant();

exporter.Export("C:\\output.txt");
_

ただし、入力値(またはそのタイプ)がエクスポーターのタイプに基づいて変化することにも気付きました。

_IExporter exporter = GetWhateverExporterYouWant();

exporter.Export("C:\\output.txt", myArrayData); // If it's ArrayExporter
exporter.Export("C:\\output.txt", myContext, myTable); // If it's DatabaseExporter
_

共通のインターフェース(Exportメソッド)は、実装ごとに異なるシグネチャを持つことができないため、ここで問題が発生します。

サブクラスは独自のコンストラクター引数を定義できることにすでに気づいています。それは非常に有効なポイントですが、そのルートを下りたくない場合があります。

これが意味を持つのは、グローバル依存関係注入を使用する場合で、コンストラクターは実際には「入力値」ではなく「注入された依存関係」を受け入れます。
技術レベルでは同じですが、機能レベルでは異なります。注入された依存関係は通常、データに依存しません(どの依存関係を使用するかの決定は、通常、外部要因に基づいて決定されます)が、入力値は現在の要求/実行に応じて変化し、常に変化することが予想されます。

ご覧のとおり、ここには3つのオプションがあります。私は3番目のオプションを支持しますが、最初に他のオプションについて説明して、なぜそれらが自分に適用できないのかを理解できるようにしたいと思います。

1.明示的なインスタンス化

コンストラクター引数アプローチを引き続き使用できますが、入力値を簡単かつ明確に変更可能にする必要があるため、ブラインドプリセットフレームワークを使用してそれを行うことはできません。

明示的なインスタンス化が1つまたは2つのライナー以上のものになる場合;良い習慣はあなたを工場の領域に押し込む傾向があります。それはあなたが現在やっていることです。

_IExporter CreateDatabaseExporter(DbContext dbContext, string tableName);
IExporter CreateArrayExporter(string[] arrayData);
_

これはこの問題への健全なアプローチですか? 1つのクラスにさまざまな構築実装をすべて含めるのは奇妙に感じられますが、それがファクトリーのポイントです。

技術的なレベルでは、これには何の問題もありません。ただし、ファクトリメソッドがパススルーに過ぎない場合、ファクトリには目的がありません。私は次のようなことについて話している:

_public IExporter CreateDatabaseExporter(DbContext dbContext, string tableName)
{
    return new DatabaseExporter(dbContext, tableName);
}

public IExporter CreateArrayExporter(string[] arrayData)
{
    return new ArrayExporter(arrayData);
}
_

ここでのファクトリーメソッドは空のシェルにすぎません。呼び出し側には、ファクトリーを使用しても何も得られません。工場で:

_IExporter exporter = ExporterFactory.CreateArrayExporter(arrayData);
_

工場なし:

_IExporter exporter = new ArrayExporter(arrayData);
_

ファクトリの目的は使用するサブクラスの決定です。発信者へのボーナスは、発信者が異なるオプションを知っているか、または決定する必要がないことです。それは単に工場に「私の目的に合ったものは何でもください」と伝えます。パススルーファクトリメソッドを作成しても、実際にはファクトリに目的はありません。

このように考えてみてください。レストランに行ってスパゲッティアラビアータを頼むとき、あなたはシェフがあなたの食事にどのスパイスを使うかを知っていると信じています。あなたはそれをすべて考え出すことに煩わされることはできません、あなたはシェフに「ちょうどそれを美味しくする」と言います。
代わりに、シェフがあなたに彼に使用してほしいすべてのスパイスをリストするように要求した場合、-そして、シェフに目的はありません(その場合は、独自のアラビアータを調理することもできます)。

公平に言うと、1つの小さな利点があります。パススルーメソッドを使用した場合でも、コンストラクターがすべての場所ではなく、1つの場所(ファクトリーメソッド)でのみ呼び出されるようにします。
しかし、それが実際に利点をもたらすかどうかは不明です。すべてのコンストラクター呼び出しを新しいクラスに変更する必要がある場合に利点がありますが、実際には、クラス名のリファクタリング(すべての参照を一度に変更する)を行うことで簡単に修正できます。

さらに、これはラクダの背筋のわらです。追加の層は、機能的な目的がなくても、必ずしも不良または逆効果であるとは限りません。しかし、同じロジックをアプリケーションのすべての部分に適用し続け、ラクダの背中にますます(無意味な)ストローを置き続けると、最終的に壊れます。

私はファクトリーの目的を再評価し、それが呼び出し側が単に自分のエクスポーターをインスタンス化するのと比較して、ファクトリーが本当に何かを追加しているかどうかを再評価します。

2.汎用入力パラメーター

専用の手荷物が付属しているため、適用できない場合があります。これを行うには、入力パラメーターのタイプを知る必要があります(これを回避するための工夫された方法がありますが、その複雑さはIMOが高すぎ、保守性と可読性を大幅に低下させます)。

_public interface IExporter<TSource>
{
    void Export(string outputPath, TSource data);
}
_

次に、インターフェースを実装するときに、具象型を使用します。

_public class PersonExporter : IExporter<Person>
{
    void Export(string outputPath, Person data) { }
}
_

複数の入力値がある場合は、すべての値を含むDTOを作成します。

_public class DatabaseValues
{
    public Context MyContext { get; set; }
    public string TableName { get; set; }
}

public class DatabaseExporter : IExporter<DatabaseValues>
{
    void Export(string outputPath, DatabaseValues data) { }
}
_

別のジェネリック型を具象クラスとして使用することもできます。

_public class ArrayExporter : IExporter<IEnumerable<T>>
{
    void Export(string outputPath, IEnumerable<T> data) { }
}
_

ただし、具象クラスに到達するまでこれをさらにサブクラス化するか、少なくともジェネリックパラメーターにいくつかのfor制約を追加することができます。

_public interface IExportableData
{
    IEnumerable<string> GetExportValues();
}

public class ArrayExporter : IExporter<IEnumerable<T>> where T : IExportableData
{
    void Export(string outputPath, IEnumerable<T> data) { }
}
_

しかし、一般的なアプローチは、一般的な_IExporter<T>_クラスに十分な共有ロジックがある場合にのみ価値があります。それ以外の場合は、それでもパススルーである中空のシェルであり、通常、無意味な中空のシェルを作成する努力の価値はありません。それは(わずかに)コードを膨らませ、機能的な値を追加せず、実際に多態的なアプローチを損なうので、常に入力値のタイプを知る必要があります(IExporterとして何かを参照することはできなくなります、常に_IExporter<Person>_、_IExporter<DatabaseValues_などとして参照する必要があります...

3.共通の基本タイプがない(および/または責任を分割する)

2つのクラスが同じメソッド名/シグネチャを持っているからといって、必ずしもそれらがmustで基本クラスを共有する必要があるわけではありません。例えば。クラスPersonDiseaseAlbumにすべてNameプロパティがあるからといって、共有のBaseNamedObjectまたはIObjectWithANameクラスから派生させる必要があるわけではありません。

同じことがあなたの輸出業者にも起こります。それはすべて1つの質問に依存します:共通の基本型からどのようなメリットを得ていますか?サンプルコードに基づいた大きなメリットはあまりありません。あなたは次のようなことを達成しようとしているように感じます:

_public class FooExporter : IExporter
{
    public void Export(string outputPath, Foo foo)
    {
        // A lot of code that parses your Foo as a string

        File.WriteAllText(outputPath, "foo as a string");
    }
}
_

これは単純化しすぎですが、エクスポーターに含まれる論理ステップがnotであり、入力データとそのタイプに本質的に関連しているのか、私は苦労しています。 exportロジック、すべてのIExporterオブジェクト間で共有されるロジックはどこにありますか?そのために継承/実装構造を作成することが本当に必要なロジックが1つまたは2つ以上あるのですか?

私の意見では、エクスポートは本来、解析/エクスポートするデータによって定義されています。作業の95%はデータの解析に費やされています。データの書き込みは、通常、.NET構成(StreamWriterまたはFile.Write...()またはFile.Append...()バリアントのいずれか)を使用するまでは、非常に簡単です。データが異なると、まったく異なるアプローチになります。ほとんどの場合、再利用可能な断片はほとんどありません。つまり、共通の基本タイプはほとんど目的を提供しません。

IExporter(および実装)は、IFileWriter型としてより意図されているように感じられます。ほとんどの場合、データをディスクに書き込む方法を定義するだけです(これは一般化できるものですが、すでにほとんど必要とされていないワンライナーです)データの解析方法ではなく、追加のラッパー)(これは、ご存じのように、実際に一般化することはできません)。

データ生成データストレージの責任を分離する方が良いかもしれません、データストレージは単一のクラス(文字列データを取り込む)になる可能性があるためです。

データ生成、つまり特定のタイプの入力パラメーターを受け取り、それをエクスポート可能な形式に解析する場合、「両方ともエクスポートするもの」と言うことができるという理由だけで、それらに基本クラスを作成しないようにする方がよい場合があります。 」エクスポートは非​​常にあいまいであり、非常に多様なトピックであるため、強力な共通点を見つけることは困難です。

2
Flater

最も強力な設計手法の1つは、いつ知っているかに注意を払うことです。これに従うことで、設計をシンプルに保つことができます。

依存関係を間違った場所に設定している可能性があります。

ArrayExporter.Export()は、string[] arrayDatastring outputPathの両方に依存します。しかし、いつ知っていますか?


残りがわかるまでoutputPathを知らない場合:

Exporter exp = new Exporter();

これらを、次のいずれかを呼び出すものに挿入します。

exp.ArrayExport(arrayData, outputPath);

exp.DBExport(dbContext, tableName, outputPath);

string[] arrayDataの前にoutputPathを知っている場合は、次の点を考慮してください。

Exporter exp = new Exporter(outputPath);

これらを、次のいずれかを呼び出すものに挿入します。

exp.ArrayExport(arrayData);

exp.DBExport(dbContext, tableName);

データベースと配列について知っている同じクラスが気に入らない場合は、ストリームアダプターを検討してください。

Exporter exp = new Exporter(outputPath);

これらを、次のいずれかを呼び出すものに挿入します。

exp.Export(new ArrayAsStream(arrayData));

exp.Export(new DatabaseAsStream(dbContext, tableName));

これで、Exporterクラスは出力ファイルとストリームのみを認識します。すべてのデータソースは、独自のストリームアダプタクラスを持つことができます。あなたが物事を学ぶ順番を変えることなくすべて。


いつエクスポートするかがわかる前に、これらすべてを十分に理解している場合:

Exporter exp = new ArrayExporter(arrayData, outputPath);

Exporter exp = new DatabaseExporter(dbContext, tableName, outputPath);

それらのいずれかを呼び出すものに注入します:

exp.Export();

必要に応じて、ここでストリームのアイデアを再利用できます。


あなたのコメント によれば、あなたは最初にarrayDataを知っています

Exporter exp = new ArrayExporter(arrayData);

Exporter exp = new DatabaseExporter(dbContext, tableName);

それらのいずれかを呼び出すものに注入します:

exp.Export(outputPath);

これらの方法のいずれでも、好きなだけ純粋な依存関係注入を使用して、心のコンテンツを模倣することができます。

6
candied_orange

これはこの問題への健全なアプローチですか?

場合によります。多くの状況では、これで十分で完全に問題ありません。 2週間ごとに新しいタイプのエクスポーターを作成しない場合、またはエクスポータークラスを処理するための拡張可能なブラックボックスライブラリを提供したくない場合は、おそらく、物事をシンプルに保ち、少しだけ「ガムテープ"。

単一のクラスにさまざまな構築実装をすべて持つのは奇妙に感じます

それが気になる場合、または工場が開閉の原則に従うことを望む場合は、abstract factoryパターン。 DatabaseExporterFactoryArrayExporterFactoryを作成し、IExporterFactoryを実装します。これで、構築の実装がさまざまなクラスで「ライブ」になり、新しいファクトリタイプを追加するだけで新しい構築メカニズムを追加できます。

パターンを使用すると、実際の作成が行われるコード内のポイントから、作成するエクスポーターのタイプを決定できます。これは、これらのインターフェースを利用する再利用可能なライブラリコードの外部に新しいエクスポータータイプと新しいエクスポーターファクトリを追加できるようにする場合に便利です。

「単一の結合された」ファクトリーが依存関係の望ましくない結合を導入する場合、異なる構築プロセスの分離が実際に必須である場合があることを付け加えておきます。 DatabaseExporterの作成コードが巨大なデータベースコンポーネントに依存している場合、そのコードをArrayExporter作成コードと同じクラスに配置すると、後者を再利用しようとするすべてのコードが強制的に依存します。データベースコンポーネント上。その場合は、「抽象ファクトリ」を使用するのが適切な解決策かもしれません。

3
Doc Brown

私たちがあなたの方法を使用する場合、これは実際にテストするのが非常に簡単です。

データベースと配列をエクスポートするインターフェースがあります。次に、インターフェースを実装する単純なモック実装を含むテストエクスポートクラスを追加して、テストに使用できるようにします。

また、これがなぜテストを難しくするのか、もう少し説明してもらえますか?

また、おそらくエクスポートインターフェイスは良い抽象化ではありませんか?

あなたは単一の責任の原則に違反しており、2つのクラスが多くのことをしているようです。

データベースから読み取るクラスが必要です。次に、そのdbの出力を受け取り、それをファイルに書き込むクラス。テストしやすくなるかもしれません。私はただアイデアを捨てているだけで、あなたが何をしているのかよくわかりません。

1

これに対する私の最初の見方は、あなたのクラスはすでにSRPに違反しており、これが直交インターフェースの構築を困難にしているということです。

interface IDataReader
{
    IEnumerable<TSchema> Read<TSchema>(DbContext dbContext, string tableName)
}

interface IDataWriter 
{
    bool Write<TSchema>(IEnumerable<TSchema> data, string outputPath);
}

すでにデータがある場合は、それを書き込むことができます。データが必要な場合は、DBからデータをプルして、書き込むことができます。

0
Jared Goguen

私の最初の衝動は、あなたが本当にIEnumerableを拡張しようとしているということです。そのため、 [〜#〜] linq [〜#〜] のようなIEnumerable拡張を書くだけです。シンプルになるだけでなく、モックやテストも簡単です!

私の提案を拡張すると、DbSet、配列、辞書、リストなどがあるかどうかに関係なく、それらはIEnumerableであるため、重要ではありません。データソースがIEnumerableである場合、それをエクスポートできるはずです。したがって、IEnumerableをIExportable(または類似の何か)で拡張する場合、LINQ構文を使用して、まだ考えていないものも含めて、任意のIEnumerableをエクスポートできます。一方、データソースの種類ごとに視覚的に異なることを行う場合は、IEnumerableの拡張が機能しないことがあります。

0
reasonet