7

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

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

我最初的想法是只使用对象初始化器。Red, Blue, 和Green是属性,并且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

4 回答 4

9

这行得通吗?

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

假设Mix()andStir()被定义为返回一个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

我会这样想:

您本质上希望链中的最后一个方法返回一个 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() 之前设置至少一种颜色。让我们用一些语法接口来强制它。

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 提供者更改了您下面的所有内容,您只需重写这段代码;所有调用代码都可以保持不变。

于 2010-03-13T16:23:15.560 回答
0

我会使用 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

如果您真的希望能够在不必编写大量代码的情况下链接属性设置,那么一种方法是使用代码生成 (CodeDom)。您可以使用反射来获取可变属性的列表,生成一个流畅的构建器类,并使用最终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);
    }
}

我认为这应该就是这样做 - 它显然未经测试,但你从这里开始创建一个代码生成(继承自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

等等。保存的时候会自动生成你需要的代码文件,支持上面写语句。

我以前曾将这种方法用于具有数百种不同消息类型的高度复杂的消息传递系统。总是构建消息、设置一堆属性、通过通道发送、从通道接收、序列化响应等花费了太长时间......使用代码生成器大大简化了工作,因为它使我能够生成一个单个消息传递类,它将所有单独的属性作为参数并返回正确类型的响应。这不是我会推荐给每个人的东西,但是当您处理非常大的项目时,有时您需要开始发明自己的语法!

于 2010-03-13T17:04:19.397 回答