流利地设置C#属性和链接方法

时间:2010-03-13 15:40:05

标签: c# xml .net-3.5 fluent-interface

我正在使用.NET 3.5。我们有一些复杂的第三方类,它们是自动生成的,不受我的控制,但我们必须将它们用于测试目的。我看到我的团队在我们的测试代码中进行了很多深度嵌套的属性获取/设置,而且它变得非常麻烦。

为了解决这个问题,我想建立一个流畅的界面来设置分层树中各种对象的属性。这个第三方库中有大量属性和类,手动映射所有内容太繁琐。

我最初的想法是只使用对象初始化器。 RedBlueGreen是属性,Mix()是将第四个属性Color设置为最接近的RGB安全颜色的方法颜色。涂料必须与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之前动态定义方法,所以我认为我不能以某种方式自动挂钩。

第三次尝试:

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

这看起来并不太糟糕,而且似乎是可行的。这是一种可行的方法吗?是否可以编写以这种方式工作的Set方法?或者我应该采取其他策略吗?

4 个答案:

答案 0 :(得分: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();
  })
};

答案 1 :(得分:5)

我会这样想:

您基本上希望链中的最后一个方法返回一个Bucket。在你的情况下,我认为你希望这个方法是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()之前,您需要设置至少一种颜色。让我们用一些Syntax接口来强制它。

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参数(只要你输入至少一个)作为奖励。

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

Fluent Interfaces的问题在于它们并不容易组合在一起,但是开发人员很容易编写代码并且它们非常易于扩展。如果你想在不改变所有调用代码的情况下为它添加一个Matt / Gloss标志,那么很容易做到。

此外,如果您的API提供商更改了您下面的所有内容,您只需要重写这一段代码;所有的callin代码都可以保持不变。

答案 2 :(得分:0)

我会使用Init扩展方法,因为U总是可以使用委托。 地狱你总是可以声明占用表达式的扩展方法,甚至可以使用表达式(将它们存储起来以供日后使用,修改,等等) 这样您就可以轻松存储默认格式:

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

这种方式您可以使用所有操作(或funcs)并将标准初始化程序缓存为表达式链以供日后使用?

答案 3 :(得分:0)

如果您真的希望能够在不必编写大量代码的情况下链接属性设置,那么执行此操作的一种方法是使用代码生成(CodeDom)。您可以使用Reflection来获取可变属性的列表,使用最终Build()方法生成一个流畅的构建器类,该方法返回您实际尝试创建的类。

我将跳过关于如何注册自定义工具的所有样板文件 - 这很容易找到文档,但仍然啰嗦,我不认为我会通过包含它来增加很多。我会告诉你我为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);
    }
}

我认为这应该就是这样做的 - 它显然是未经测试的,但是你从这里开始的是你创建了一个codegen(继承自BaseCodeGeneratorWithSite),它编译了一个填充了列表的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极大地简化了工作,因为它使我能够生成单个消息传递类,它将所有单个属性作为参数并吐出正确类型的响应。这不是我向所有人推荐的东西,但是当你处理非常大的项目时,有时你需要开始发明你自己的语法!