6

今まで経験したことのない奇妙な設計状況があります... Objective-C を使用していた場合は、カテゴリで解決しますが、C# 2.0 を使用する必要があります。

まず、いくつかの背景。このクラス ライブラリには 2 つの抽象化レイヤーがあります。最下層は、コンテンツをスキャンするコンポーネントのプラグイン アーキテクチャを実装します (申し訳ありませんが、それ以上具体的にはできません)。各プラグインは独自の方法でスキャンを行いますが、受け入れるコンテンツの種類によってプラグインが異なる場合もあります。この議論とは関係のないさまざまな理由から、プラグイン インターフェイスを介して Generics を公開したくありませんでした。そのため、コンテンツ タイプごとに IScanner インターフェイスと派生インターフェイスを作成しました。

最上層は、さまざまな部分を含む複合コンテンツ形式を受け入れる便利なラッパーです。スキャナーが異なれば、関心のあるコンテンツ タイプに応じて、コンポジットのさまざまな部分が必要になります。したがって、必要な関連部分を探して、コンポジット コンテンツを解析する IScanner 派生インターフェイスごとに固有のロジックを用意する必要があります。

これを解決する 1 つの方法は、単純に別のメソッドを IScanner に追加して、各プラグインに実装することです。ただし、2 層設計の要点は、プラグイン自体が複合フォーマットについて知る必要がないようにすることです。これを解決するための強引な方法は、上位層に型テストとダウンキャストを配置することですが、将来的に新しいコンテンツ タイプのサポートが追加されるため、これらは慎重に維持する必要があります。実際には Visitor は 1 つしかないため、Visitor パターンもこの状況では扱いにくいですが、さまざまな Visitable タイプの数は時間の経過とともに増加するだけです (つまり、これらは Visitor が適している反対の条件です)。さらに、IScanner のシングル ディスパッチをハイジャックしたいだけなのに、ダブル ディスパッチはやり過ぎのように感じます。

Objective-C を使用している場合は、IScanner から派生した各インターフェイスでカテゴリを定義し、そこに parseContent メソッドを追加します。カテゴリは上位層で定義されるため、プラグインを変更する必要はなく、同時に型テストの必要もありません。残念ながら、C# 拡張メソッドは基本的に静的であるため機能しません (つまり、呼び出しサイトで使用される参照のコンパイル時の型に関連付けられており、Obj-C カテゴリのような動的ディスパッチにフックされていません)。言うまでもなく、C# 2.0 を使用する必要があるため、拡張メソッドを使用することさえできません。:-P

この問題を C# で解決するためのクリーンでシンプルな方法はありますか。


編集:現在の設計の構造を明確にするのに役立ついくつかの擬似コード:

interface IScanner
{ // Nothing to see here...
}

interface IContentTypeAScanner : IScanner
{
    void ScanTypeA(TypeA content);
}

interface IContentTypeBScanner : IScanner
{
    void ScanTypeB(TypeB content);
}

class CompositeScanner
{
    private readonly IScanner realScanner;

    // C-tor omitted for brevity... It takes an IScanner that was created
    // from an assembly-qualified type name using dynamic type loading.

    // NOTE: Composite is defined outside my code and completely outside my control.
    public void ScanComposite(Composite c)
    {
        // Solution I would like (imaginary syntax borrowed from Obj-C):
        // [realScanner parseAndScanContentFrom: c];
        // where parseAndScanContentFrom: is defined in a category for each
        // interface derived from IScanner.

        // Solution I am stuck with for now:
        if (realScanner is IContentTypeAScanner)
        {
            (realScanner as IContentTypeAScanner).ScanTypeA(this.parseTypeA(c));
        }
        else if (realScanner is IContentTypeBScanner)
        {
            (realScanner as IContentTypeBScanner).ScanTypeB(this.parseTypeB(c));
        }
        else
        {
            throw new SomeKindOfException();
        }
    }

    // Private parsing methods omitted for brevity...
}

編集: 明確にするために、私はこのデザインについてすでに多くのことを考えてきました。私には多くの理由がありますが、そのほとんどを共有することはできません。興味深いのですが、元の質問をかわしているため、まだ回答を受け入れていません。

実際、Obj-C では、この問題をシンプルかつエレガントに解決できました。問題は、C# で同じ手法を使用できるかということです。代替案を探すのは構いませんが、公平を期すために、それは私が尋ねた質問ではありません。:)

4

2 に答える 2

1

あなたが言っているのは、コンテンツが次のようにレイアウトされているということのようです。

+--------+
| | パート1 |
| | タイプA |
+--------+
| | パート 2 |
| | タイプ C |
+--------+
| | パート 3 |
| | タイプ F |
+--------+
| | パート4 |
| | タイプD |
+--------+

パーツ タイプごとにリーダーがあります。つまり、AScanner はタイプ A の一部 (上記のパート 1 など) のデータを処理する方法を知っており、BScanner はタイプ B の一部のデータを処理する方法を知っている、などです。私は今のところ正しいですか?

さて、あなたが抱えている問題は、型リーダー (IScanner実装) が、複合コンテナー内で認識するパーツを見つける方法がわからないことです。

複合コンテナは、別々の部分を正しく列挙できますか (つまり、ある部分がどこで終わり、別の部分が始まるかを認識していますか)、そうであれば、各部分はスキャナまたはコンテナのいずれかが区別できる何らかの識別を持っていますか?

つまり、データはこのように配置されていますか?

+-------------+
| | パート1 |
| | 長さ: 100 |
| | タイプ: "A" |
| | データ: ... |
+-------------+
| | パート 2 |
| | 長さ: 460 |
| | タイプ: "C" |
| | データ: ... |
+-------------+
| | パート 3 |
| | 長さ: 26 |
| | タイプ: "F" |
| | データ: ... |
+-------------+
| | パート4 |
| | 長さ: 790 |
| | タイプ: "D" |
| | データ: ... |
+-------------+

データ レイアウトがこれに似ている場合、スキャナは特定のパターンに一致する識別子を持つすべてのパーツをコンテナに要求できないでしょうか? 何かのようなもの:

class Container : IContainer{
    IEnumerable IContainer.GetParts(string type){
        foreach(IPart part in internalPartsList)
            if(part.TypeID == type)
                yield return part;
    }
}

class AScanner : IScanner{
    void IScanner.ProcessContainer(IContainer c){
        foreach(IPart part in c.GetParts("A"))
            ProcessPart(part);
    }
}

または、コンテナーがパーツの種類を認識できない場合でも、スキャナーは独自のパーツの種類を認識できる場合は、次のようになります。

delegate void PartCallback(IPart part);

class Container : IContainer{
    void IContainer.GetParts(PartCallback callback){
        foreach(IPart part in internalPartsList)
            callback(part);
    }
}

class AScanner : IScanner{
    void IScanner.ProcessContainer(IContainer c){
        c.GetParts(delegate(IPart part){
            if(IsTypeA(part))
                ProcessPart(part);
        });
    }

    bool IsTypeA(IPart part){
        // determine if part is of type A
    }
}

おそらく、私はあなたのコンテンツやアーキテクチャを誤解しています。もしそうなら、明確にしてください、そして私は更新します。


OPからのコメント:

  1. スキャナーはコンテナーの種類を認識しません。
  2. コンテナー タイプには組み込みのインテリジェンスがありません。これは、C# で取得できる従来のデータに限りなく近いものです。
  3. コンテナーの種類を変更できません。これは、既存のアーキテクチャの一部です。

私の応答はコメントするには長すぎます:

  1. スキャナーには、処理するパーツを取得する何らかの方法が必要です。将来インターフェースを自由に変更IScannerできるように、インターフェースがそのインターフェースを認識するべきではないという懸念がある場合は、いくつかの方法のいずれかで妥協することができます。IContainerIContainer

    • から派生した (または含まれている)IPartProviderインターフェースをスキャナーに渡すことができます。IContainerこれIPartProviderはパーツを提供する機能のみを提供するため、非常に安定している必要があり、 と同じアセンブリで定義できるIScannerため、プラグインはIContainerが定義されているアセンブリを参照する必要がありません。
    • パーツの取得に使用できるデリゲートをスキャナーに渡すことができます。IScannerスキャナは、デリゲートのみを除いて、インターフェイスの知識をまったく必要としません。
  2. コンテナとスキャナの両方と通信する方法を知っているサロゲート クラスが必要なようです。外部/派生クラスが関連データにアクセスできるように、コンテナがすでに十分な機能をパブリックに (または保護されて [それは言葉ですか?]) 公開している限り、上記の機能のいずれも任意の古いクラスに実装できます。 .

編集した質問の疑似コードから、インターフェースから実際には何の利益も得ていないように見え、プラグインをメインアプリに緊密に結合しているように見えますIScanner. " メソッドであり、CompositeScannerクラスにはパーツ タイプごとに一意の "parse" メソッドがあります。

これがあなたの主な問題だと思います。プラグイン (IScannerインターフェイスのインプリメンターであると想定) をメイン アプリ (CompositeScannerクラスが存在する場所であると想定) から分離する必要があります。私の以前の提案の 1 つは、それを実装する方法ですが、正確な詳細は、parseTypeX関数がどのように機能するかによって異なります。これらを抽象化して一般化できますか?

おそらく、parseTypeX関数はCompositeクラス オブジェクトと通信して、必要なデータを取得します。これらを、オブジェクトからこのデータを取得するためにクラスを介してプロキシするインターフェイスのParseメソッドに移動できないでしょうか? このようなもの:IScannerCompositeScannerComposite

delegate byte[] GetDataHandler(int offset, int length);

interface IScanner{
    void   Scan(byte[] data);
    byte[] Parse(GetDataHandler getData);
}

class Composite{
    public byte[] GetData(int offset, int length){/*...*/}
}

class CompositeScanner{}
    IScanner realScanner;

    public void ScanComposite(Composite c){
        realScanner.Scan(realScanner.Parse(delegate(int offset, int length){
            return c.GetData(offset, length);
        });
    }
}

もちろん、これは別のParseメソッド onを削除し、単にデリゲートを直接IScanner渡すことで簡素化できます(必要に応じて、その実装で private を呼び出すことができます)。コードは、以前の例と非常によく似ています。GetDataHandlerScanParse

この設計は、考えられる限りの関心事の分離とデカップリングを提供します。


私は、あなたがより口当たりが良く、実際、懸念事項のより良い分離を提供するかもしれない何か他のものを考えました.

各プラグインをアプリケーションに「登録」できる場合は、プラグインがデータの取得方法をアプリケーションに伝えることができる限り、解析をアプリケーション内に残すことができます。例を以下に示しますが、パーツがどのように識別されるかがわからないため、2 つの可能性を実装しました。1 つはインデックス付きパーツ用、もう 1 つは名前付きパーツ用です。

// parts identified by their offset within the file
class MainApp{
    struct BlockBounds{
        public int offset;
        public int length;

        public BlockBounds(int offset, int length){
            this.offset = offset;
            this.length = length;
        }
    }

    Dictionary<Type, BlockBounds> plugins = new Dictionary<Type, BlockBounds>();

    public void RegisterPlugin(Type type, int offset, int length){
        plugins[type] = new BlockBounds(offset, length);
    }

    public void ScanContent(Container c){
        foreach(KeyValuePair<Type, int> pair in plugins)
            ((IScanner)Activator.CreateInstance(pair.Key)).Scan(
                c.GetData(pair.Value.offset, pair.Value.length);
    }
}

また

// parts identified by name, block length stored within content (as in diagram above)
class MainApp{
    Dictionary<string, Type> plugins = new Dictionary<string, Type>();

    public void RegisterPlugin(Type type, string partID){
        plugins[partID] = type;
    }

    public void ScanContent(Container c){
        foreach(IPart part in c.GetParts()){
            Type type;
            if(plugins.TryGetValue(part.ID, out type))
                ((IScanner)Activator.CreateInstance(type)).Scan(part.Data);
        }
    }
}

もちろん、これらの例は非常に単純化されていますが、理解していただければ幸いです。さらに、 を使用するのではなく、ファクトリ (またはファクトリ デリゲート) をメソッドActivator.CreateInstanceに渡すことができれば便利です。RegisterPlugin

于 2009-01-07T17:35:51.820 に答える
1

試してみます... ;-) システムに、オブジェクトの「カタログ」を作成するフェーズがある場合は、オブジェクトが興味を持っていることを示す属性IScannerで を装飾することを考えることができます。その後、マップできます。この情報を使用して、マップを使用してスキャンを実行します。これは完全な答えではありません。少し時間があれば、詳しく説明します...IScannerPartComposite

編集:私の混乱した説明をサポートするための少しの疑似コード

public interface IScanner
{
    void Scan(IPart part);
}

public interface IPart
{
    string ID { get; }
}

[ScannedPart("your-id-for-A")]
public class AlphaScanner : IScanner
{
    public void Scan(IPart part)
    {
        throw new NotImplementedException();
    }
}

[ScannedPart("your-id-for-B")]
public class BetaScanner : IScanner
{
    public void Scan(IPart part)
    {
        throw new NotImplementedException();
    }
}

public interface IComposite
{
    List<IPart> Parts { get; }
}

public class ScannerDriver
{
    public Dictionary<string, IScanner> Scanners { get; private set; }

    public void DoScan(IComposite composite)
    {
        foreach (IPart part in composite.Parts)
        {
            IScanner scanner = Scanners[part.ID];
            scanner.Scan(part);
        }
    }
}

説明のためのものです。

編集:カーネル大佐のコメントへの回答。面白いと思っていただけたら幸いです。:-) コード リフレクションのこの簡単なスケッチでは、ディクショナリの初期化中 (または必要なとき) にのみ関与する必要があり、このフェーズ中に属性の存在を「強制」できます (または、スキャナーとパーツをマッピングする他の方法を使用することもできます)。「強制する」と言ったのは、コンパイル時の制約でなくても、コードを本番環境に置く前に少なくとも 1 回は実行すると思うからです;-) 必要に応じて実行時の制約になる可能性があります。インスピレーションは、 MEFや他の同様のフレームワークに (非常に軽く) 似ていると言えます。ちょうど私の2セント。

于 2009-01-07T17:44:47.170 に答える