【问题标题】:Fluently setting C# properties and chaining methods流畅地设置 C# 属性和链接方法
【发布时间】:2011-01-27 04:34:04
【问题描述】:

我正在使用 .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 方法?还是我应该采用另一种策略?

【问题讨论】:

  • 其实我的一个项目也有类似的方法。

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


【解决方案1】:

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

我将跳过所有关于如何注册自定义工具的样板文件 - 这很容易找到文档,但仍然冗长,我认为包含它不会增加太多内容。不过,我将向您展示我对代码生成的想法。

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

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

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

【讨论】:

    【解决方案2】:

    我会这样想:

    您本质上希望链中的最后一个方法返回一个 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 提供者改变了你下面的一切,你只需要重写这段代码;所有调用代码都可以保持不变。

    【讨论】:

      【解决方案3】:

      我会使用 Init 扩展方法,因为 U 总是可以与代理一起玩。 地狱您总是可以声明占用表达式的扩展方法,甚至可以使用表达式(将它们存储起来以备后用,修改等等) 这样您就可以轻松存储默认组,例如:

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

      这样你可以使用所有的动作(或函数)和缓存标准初始化器作为表达式链供以后使用?

      【讨论】:

        【解决方案4】:

        这行得通吗?

        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();
          })
        };
        

        【讨论】:

        • 回想起来,这似乎是一种显而易见的方法。盯着这个看太久了,我一定是只见树木不见森林!
        • 对于那些对@this 感到困惑的人来说,仅供参考:@ 的意思是“将以下标记视为文字标识符”(而不是关键字)。效果是命名一个名为this的参数。
        • 我对此并不感到困惑,但是当您可以使用像 selfsource 这样的等效名称时,我认为没有任何理由在这里与编译器抗衡。否则,好的答案和 +1。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-07-09
        • 1970-01-01
        • 2013-10-30
        • 2011-09-18
        相关资源
        最近更新 更多