7

.NET3.5を使用しています。いくつかの複雑なサードパーティのクラスがあります。これらは自動的に生成され、制御できなくなりますが、テスト目的で使用する必要があります。私のチームがテストコードで多くの深くネストされたプロパティの取得/設定を行っているのを見て、それはかなり面倒になっています。

この問題を解決するために、階層ツリー内のさまざまなオブジェクトにプロパティを設定するための流暢なインターフェイスを作成したいと思います。このサードパーティライブラリには多数のプロパティとクラスがあり、すべてを手動でマッピングするのは面倒です。

私の最初の考えは、オブジェクト初期化子を使用することでした。Red、、、BlueおよびGreenはプロパティであり、4番目のプロパティをその混合色で最も近いRGBセーフカラーにMix()設定するメソッドです。塗料は、使用する前にColor均質化する必要があります。Stir()

Bucket b = new Bucket() {
  Paint = new Paint() {
    Red = 0.4;
    Blue = 0.2;
    Green = 0.1;
  }
};

これはを初期化するために機能しますが、チェーンやその他のメソッドをそれPaintにチェーンする必要があります。Mix()次の試み:

Create<Bucket>(Create<Paint>()
  .SetRed(0.4)
  .SetBlue(0.2)
  .SetGreen(0.1)
  .Mix().Stir()
)

ただし、設定するプロパティごとにメソッドを定義する必要があり、すべてのクラスに何百もの異なるプロパティがあるため、これは適切にスケーリングされません。また、C#にはC#4より前のメソッドを動的に定義する方法がないため、何らかの方法でこれを自動的に行うためにフックすることはできないと思います。

3回目の試行:

Create<Bucket>(Create<Paint>().Set(p => {
    p.Red = 0.4;
    p.Blue = 0.2;
    p.Green = 0.1;
  }).Mix().Stir()
)

それはそれほど悪くはないように見えます、そしてそれは実行可能であるように思われます。これは推奨されるアプローチですか?Setこのように機能するメソッドを作成することは可能ですか?それとも、別の戦略を追求する必要がありますか?

4

4 に答える 4

9

これは機能しますか?

Bucket b = new Bucket() {
  Paint = new Paint() {
    Red = 0.4;
    Blue = 0.2;
    Green = 0.1;
  }.Mix().Stir()
};

と仮定Mix()Stir()て、オブジェクトを返すように定義されていPaintます。

を返すメソッドを呼び出すvoidには、渡すオブジェクトに対して追加の初期化を実行できる拡張メソッドを使用できます。

public static T Init<T>(this T @this, Action<T> initAction) {
    if (initAction != null)
        initAction(@this);
    return @this;
}

これは、説明されているようにSet()と同様に使用できます。

Bucket b = new Bucket() {
  Paint = new Paint() {
    Red = 0.4;
    Blue = 0.2;
    Green = 0.1;
  }.Init(p => {
    p.Mix().Stir();
  })
};
于 2010-03-13T15:44:55.050 に答える
5

私はそれをこのように考えるでしょう:

基本的に、チェーン内の最後のメソッドがバケットを返す必要があります。あなたの場合、後でバケットをStir()できるので、そのメソッドをMix()にする必要があると思います。

public class BucketBuilder
{
    private int _red = 0;
    private int _green = 0;
    private int _blue = 0;

    public Bucket Mix()
    {
        Bucket bucket = new Bucket(_paint);
        bucket.Mix();
        return bucket;
    }
}

したがって、Mix()を呼び出す前に、少なくとも1つの色を設定する必要があります。いくつかの構文インターフェースでそれを強制しましょう。

public interface IStillNeedsMixing : ICanAddColours
{
     Bucket Mix();
}

public interface ICanAddColours
{
     IStillNeedsMixing Red(int red);
     IStillNeedsMixing Green(int green);
     IStillNeedsMixing Blue(int blue);
}

そして、これらをBucketBuilderに適用しましょう

public class BucketBuilder : IStillNeedsMixing, ICanAddColours
{
    private int _red = 0;
    private int _green = 0;
    private int _blue = 0;

    public IStillNeedsMixing Red(int red)
    {
         _red += red;
         return this;
    }

    public IStillNeedsMixing Green(int green)
    {
         _green += green;
         return this;
    }

    public IStillNeedsMixing Blue(int blue)
    {
         _blue += blue;
         return this;
    }

    public Bucket Mix()
    {
        Bucket bucket = new Bucket(new Paint(_red, _green, _blue));
        bucket.Mix();
        return bucket;
    }
}

ここで、チェーンを開始するための初期静的プロパティが必要です

public static class CreateBucket
{
    public static ICanAddColours UsingPaint
    {
        return new BucketBuilder();
    }
}

これでほぼ完了です。ボーナスとして、オプションのRGBパラメーター(少なくとも1つ入力する限り)を備えた流暢なインターフェースが得られます。

CreateBucket.UsingPaint.Red(0.4).Green(0.2).Mix().Stir();

Fluent Interfacesの特徴は、それらを組み合わせるのはそれほど簡単ではないということですが、開発者がコーディングするのは簡単で、非常に拡張性があります。すべての呼び出しコードを変更せずにこれにマット/グロスフラグを追加したい場合は、簡単に行うことができます。

また、APIのプロバイダーがあなたの下にあるすべてのものを変更した場合、この1つのコードを書き直すだけで済みます。すべてのコールインコードは同じままにすることができます。

于 2010-03-13T16:23:15.560 に答える
0

Uは常にデリゲートと遊ぶことができるので、私はInit拡張メソッドを使用します。地獄あなたはいつでも式を取り上げる拡張メソッドを宣言することができ、表現で遊ぶことさえできます(後でそれらを保存し、変更するなど)この方法で次のようなデフォルトのグループを簡単に保存できます:

Create<Paint>(() => new Paint{p.Red = 0.3, p.Blue = 0.2, p.Green = 0.1}).
Init(p => p.Mix().Stir())

このようにして、すべてのアクション(または関数)を使用し、標準の初期化子を後で使用するための式チェーンとしてキャッシュできますか?

于 2010-03-13T16:23:41.507 に答える
0

大量のコードを記述せずにプロパティ設定をチェーンできるようにしたい場合、これを行う1つの方法は、コード生成(CodeDom)を使用することです。Reflectionを使用して、可変プロパティのリストを取得し、Build()実際に作成しようとしているクラスを返すfinalメソッドを使用して流暢なビルダークラスを生成できます。

カスタムツールの登録方法に関する定型的な内容はすべてスキップします。ドキュメントを見つけるのはかなり簡単ですが、それでも時間がかかり、それを含めることで多くを追加することはないと思います。私がcodegenについて考えていることをお見せします。

public static class PropertyBuilderGenerator
{
    public static CodeTypeDeclaration GenerateBuilder(Type destType)
    {
        if (destType == null)
            throw new ArgumentNullException("destType");
        CodeTypeDeclaration builderType = new
            CodeTypeDeclaration(destType.Name + "Builder");
        builderType.TypeAttributes = TypeAttributes.Public;
        CodeTypeReference destTypeRef = new CodeTypeReference(destType);
        CodeExpression resultExpr = AddResultField(builderType, destTypeRef);
        PropertyInfo[] builderProps = destType.GetProperties(
            BindingFlags.Instance | BindingFlags.Public);
        foreach (PropertyInfo prop in builderProps)
        {
            AddPropertyBuilder(builderType, resultExpr, prop);
        }
        AddBuildMethod(builderType, resultExpr, destTypeRef);
        return builderType;
    }

    private static void AddBuildMethod(CodeTypeDeclaration builderType,
        CodeExpression resultExpr, CodeTypeReference destTypeRef)
    {
        CodeMemberMethod method = new CodeMemberMethod();
        method.Attributes = MemberAttributes.Public | MemberAttributes.Final;
        method.Name = "Build";
        method.ReturnType = destTypeRef;
        method.Statements.Add(new MethodReturnStatement(resultExpr));
        builderType.Members.Add(method);
    }

    private static void AddPropertyBuilder(CodeTypeDeclaration builderType,
        CodeExpression resultExpr, PropertyInfo prop)
    {
        CodeMemberMethod method = new CodeMemberMethod();
        method.Attributes = MemberAttributes.Public | MemberAttributes.Final;
        method.Name = prop.Name;
        method.ReturnType = new CodeTypeReference(builderType.Name);
        method.Parameters.Add(new CodeParameterDeclarationExpression(prop.Type,
            "value"));
        method.Statements.Add(new CodeAssignStatement(
            new CodePropertyReferenceExpression(resultExpr, prop.Name),
            new CodeArgumentReferenceExpression("value")));
        method.Statements.Add(new MethodReturnStatement(
            new CodeThisExpression()));
        builderType.Members.Add(method);
    }

    private static CodeFieldReferenceExpression AddResultField(
        CodeTypeDeclaration builderType, CodeTypeReference destTypeRef)
    {
        const string fieldName = "_result";
        CodeMemberField resultField = new CodeMemberField(destTypeRef, fieldName);
        resultField.Attributes = MemberAttributes.Private;
        builderType.Members.Add(resultField);
        return new CodeFieldReferenceExpression(
            new CodeThisReferenceExpression(), fieldName);
    }
}

これで十分だと思います-明らかにテストされていませんが、ここから先は、型のリストが入力されたBaseCodeGeneratorWithSiteものをコンパイルするcodegen(から継承)を作成することです。CodeCompileUnitこのリストは、ツールに登録したファイルタイプから取得されます。この場合、ビルダーコードを生成するタイプの行区切りリストを含むテキストファイルにするだけです。ツールにこれをスキャンさせ、タイプをロードし(最初にアセンブリをロードする必要がある場合があります)、バイトコードを生成します。

大変ですが、思ったほど難しくはありません。完了すると、次のようなコードを記述できるようになります。

Paint p = new PaintBuilder().Red(0.4).Blue(0.2).Green(0.1).Build().Mix.Stir();

私が信じているのは、ほぼ正確にあなたが望むものです。コード生成を呼び出すために必要なのは、ツールをカスタム拡張子(たとえば.buildertypes)で登録し、その拡張子を持つファイルをプロジェクトに配置し、その中にタイプのリストを配置することだけです。

MyCompany.MyProject.Paint
MyCompany.MyProject.Foo
MyCompany.MyLibrary.Bar

等々。保存すると、上記のようなステートメントの記述をサポートする必要なコードファイルが自動的に生成されます。

私は以前、数百の異なるメッセージタイプを持つ非常に複雑なメッセージングシステムにこのアプローチを使用しました。常にメッセージを作成し、一連のプロパティを設定し、チャネルを介して送信し、チャネルから受信し、応答をシリアル化するなど、時間がかかりすぎました。codegenを使用すると、生成が可能になるため、作業が大幅に簡素化されました。個々のプロパティをすべて引数として受け取り、正しいタイプの応答を吐き出す単一のメッセージングクラス。これは私がすべての人に推奨するものではありませんが、非常に大規模なプロジェクトを扱っている場合は、独自の構文を考案し始める必要がある場合があります。

于 2010-03-13T17:04:19.397 に答える