代码之家  ›  专栏  ›  技术社区  ›  John Feminella

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

  •  7
  • John Feminella  · 技术社区  · 14 年前

    我用的是.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 ,但我需要锁链 混合() 以及其他方法。下一次尝试:

    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 回复  |  直到 14 年前
        1
  •  9
  •   dahlbyk    14 年前

    这有用吗?

    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();
      })
    };
    
        2
  •  5
  •   pdr    14 年前

    我会这样想:

    实际上,您希望链中的最后一个方法返回一个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接口的特点是它们不太容易组合在一起,但是开发人员很容易对它们进行编码,而且它们是非常可扩展的。如果您想添加一个matt/gloss标志而不更改所有的调用代码,这很容易做到。

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

        3
  •  0
  •   luckyluke    14 年前

    我会使用init扩展方法,因为你总是可以使用委托。 你总是可以声明扩展方法来使用表达式,甚至可以使用表达式(存储它们以备以后使用,修改等等)。 这样您就可以轻松地存储默认的grup,比如:

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

    这样,您就可以使用所有操作(或func)和缓存标准初始值设定项作为稍后的表达式链?

        4
  •  0
  •   Aaronaught    14 年前

    如果您真的想在不编写大量代码的情况下链接属性设置,一种方法是使用代码生成(codedom)。您可以使用反射获取可变属性的列表,生成一个带有final 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
    

    等等。保存时,它将自动生成所需的代码文件,该文件支持编写上述语句。

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