简体   繁体   English

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

[英]Fluently setting C# properties and chaining methods

I'm using .NET 3.5. 我正在使用.NET 3.5。 We have some complex third-party classes which are automatically generated and out of my control, but which we must work with for testing purposes. 我们有一些复杂的第三方类,它们是自动生成的,不受我的控制,但我们必须将它们用于测试目的。 I see my team doing a lot of deeply-nested property getting/setting in our test code, and it's getting pretty cumbersome. 我看到我的团队在我们的测试代码中进行了很多深度嵌套的属性获取/设置,而且它变得非常麻烦。

To remedy the problem, I'd like to make a fluent interface for setting properties on the various objects in the hierarchical tree. 为了解决这个问题,我想建立一个流畅的界面来设置分层树中各种对象的属性。 There are a large number of properties and classes in this third-party library, and it would be too tedious to map everything manually. 这个第三方库中有大量属性和类,手动映射所有内容太繁琐了。

My initial thought was to just use object initializers. 我最初的想法是只使用对象初始化器。 Red , Blue , and Green are properties, and Mix() is a method that sets a fourth property Color to the closest RGB-safe color with that mixed color. RedBlueGreen是属性,而Mix()是一种方法,它将第四个属性Color设置为具有该混合颜色的最接近的RGB安全颜色。 Paints must be homogenized with Stir() before they can be used. 涂料在使用前必须用Stir()均质化。

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

That works to initialize the Paint , but I need to chain Mix() and other methods to it. 这可以初始化Paint ,但我需要将Mix()和其他方法链接到它。 Next attempt: 下一次尝试:

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

But that doesn't scale well, because I'd have to define a method for each property I want to set, and there are hundreds of different properties in all the classes. 但这不能很好地扩展,因为我必须为我想要设置的每个属性定义一个方法,并且所有类中都有数百个不同的属性。 Also, C# doesn't have a way to dynamically define methods prior to C# 4, so I don't think I can hook into things to do this automatically in some way. 另外,C#没有办法在C#4之前动态定义方法,所以我认为我不能以某种方式自动挂钩。

Third attempt: 第三次尝试:

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

That doesn't look too bad, and seems like it'd be feasible. 这看起来并不太糟糕,似乎它是可行的。 Is this an advisable approach? 这是一种可行的方法吗? Is it possible to write a Set method that works this way? 是否可以编写以这种方式工作的Set方法? Or should I be pursuing an alternate strategy? 或者我应该采取其他策略吗?

Does this work? 这有用吗?

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

Assuming Mix() and Stir() are defined to return a Paint object. 假设Mix()Stir()被定义为返回一个Paint对象。

To call methods that return void , you can use an extension method that will allow you to perform additional initialization on the object you pass in: 要调用返回void方法,可以使用扩展方法,该方法允许您对传入的对象执行其他初始化:

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

Which could be used similar to Set() as described: 可以使用类似于Set()的描述:

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

I would think of it this way: 我会这样想的:

You essentially want your last method in the chain to return a Bucket. 您基本上希望链中的最后一个方法返回一个Bucket。 In your case, I think you want that method to be Mix(), as you can Stir() the bucket afterwards 在你的情况下,我认为你希望这个方法是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;
    }
}

So you need to set at least one colour before you call Mix(). 因此,在调用Mix()之前,您需要设置至少一种颜色。 Let's force that with some Syntax interfaces. 让我们用一些Syntax接口来强制它。

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

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

And let's apply these to the BucketBuilder 让我们将它们应用于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;
    }
}

Now you need an initial static property to kick off the chain 现在你需要一个初始静态属性来启动链

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

And that's pretty much it, you now have a fluent interface with optional RGB parameters (as long as you enter at least one) as a bonus. 这就是它,你现在有一个流畅的界面与可选的RGB参数(只要你输入至少一个)作为奖金。

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

The thing with Fluent Interfaces is that they're not that easy to put together, but they are easy for the developer to code against and they are very extensible. Fluent Interfaces的问题在于它们并不容易组合在一起,但是开发人员很容易编写代码并且它们非常易于扩展。 If you want to add a Matt/Gloss flag to this without changing all of your calling code, it's easy to do. 如果你想在不改变所有调用代码的情况下为其添加Matt / Gloss标志,那么很容易做到。

Also, if the provider of your API changes everything underneath you, you only have to rewrite this one piece of code; 此外,如果您的API提供商更改了您下面的所有内容,您只需要重写这一段代码; all the callin code can remain the same. 所有的callin代码都可以保持不变。

I would use the Init extension method because U can always play with the delegate. 我会使用Init扩展方法,因为U总是可以使用委托。 Hell You can always declare extension methods that take up expressions and even play up with the expresions (store them for later, modify, whatever) This way You can easily store default grups like: 地狱你总是可以声明占用表达式的扩展方法,甚至可以使用表达式进行操作(将它们存储起来以供日后使用,修改,等等)这种方式您可以轻松地存储默认格式,例如:

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

This Way You can use all the actions (or funcs) and cache standard initializers as expression chains for later? 这种方式您可以使用所有操作(或funcs)并将标准初始化程序缓存为表达式链以供日后使用?

If you really want to be able to chain property settings without having to write a ton of code, one way to do this would be to use code generation (CodeDom). 如果你真的希望能够在不必编写大量代码的情况下链接属性设置,那么实现此目的的一种方法是使用代码生成(CodeDom)。 You can use Reflection to get a list of the mutable properties, the generate a fluent builder class with a final Build() method that returns the class you're actually trying to create. 您可以使用Reflection来获取可变属性的列表,使用最终的Build()方法生成一个流畅的构建器类,该方法返回您实际尝试创建的类。

I'm going to skip over all the boilerplate stuff about how to register the custom tool - that's fairly easy to find documentation on but still long-winded and I don't think I'd be adding much by including it. 我将跳过关于如何注册自定义工具的所有样板文件 - 这很容易找到文档,但仍然冗长,我不认为我会通过包含它来增加很多。 I will show you what I'm thinking of for the codegen though. 我会告诉你我为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);
    }
}

I think this should just about do it - it's obviously untested, but where you go from here is that you create a codegen (inheriting from BaseCodeGeneratorWithSite ) that compiles a CodeCompileUnit populated with a list of types. 我认为这应该只是这样做 - 它显然是未经测试的,但是你从这里开始的是你创建了一个BaseCodeGeneratorWithSite (继承自BaseCodeGeneratorWithSite ),它编译了一个填充了类型列表的CodeCompileUnit That list comes from the file type you register with the tool - in this case I'd probably just make it a text file with a line-delimited list of types that you want to generate builder code for. 该列表来自您使用该工具注册的文件类型 - 在这种情况下,我可能只是将其作为一个文本文件,其中包含要为其生成构建器代码的行划分的类型列表。 Have the tool scan this, load the types (might have to load the assemblies first), and generate bytecode. 让工具扫描这个,加载类型(可能必须先加载程序集),然后生成字节码。

It's tough, but not as tough as it sounds, and when you're done you'll be able to write code like this: 这很难,但并不像听起来那么强硬,当你完成后,你将能够编写如下代码:

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

Which I believe is almost exactly what you want. 我相信这几乎就是你想要的。 All you have to do to invoke the code generation is register the tool with a custom extension (let's say .buildertypes ), put a file with that extension in your project, and put a list of types in it: 要调用代码生成,您需要做的就是使用自定义扩展(例如.buildertypes )注册该工具,在项目中放置具有该扩展名的文件,并在其中放入类型列表:

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

And so on. 等等。 When you save, it will automatically generate the code file you need that supports writing statements like the one above. 保存时,它会自动生成所需的代码文件,支持编写如上所述的语句。

I've used this approach before for a highly convoluted messaging system with several hundred different message types. 我之前使用过这种方法,用于具有数百种不同消息类型的高度复杂的消息传递系统。 It was taking too long to always construct the message, set a bunch of properties, send it through the channel, receive from the channel, serialize the response, etc... using a codegen greatly simplified the work as it enabled me to generate a single messaging class that took all of the individual properties as arguments and spit back a response of the correct type. 总是构建消息,设置一堆属性,通过通道发送,从通道接收,序列化响应等等花费太长时间...使用codegen极大地简化了工作,因为它使我能够生成单个消息传递类,它将所有单个属性作为参数并吐出正确类型的响应。 It's not something I would recommend to everyone, but when you're dealing with very large projects, sometimes you need to start inventing your own syntax! 这不是我向所有人推荐的东西,但是当你处理非常大的项目时,有时你需要开始发明自己的语法!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM