【问题标题】:create fluent interface for adding elements to a list创建用于将元素添加到列表的流畅界面
【发布时间】:2012-04-27 08:54:13
【问题描述】:

这就是我想要实现的目标:

config.Name("Foo")
      .Elements(() => {
                         Element.Name("element1").Height(23);
                         Element.Name("element2").Height(31);
                      })
     .Foo(23);

或者像这样:

  .Elements(e => {
                     e.Name("element1").Height(23);
                     e.Name("element2").Height(31);
                  })
  .Foo(3232);

这是我目前拥有的:

public class Config
{
   private string name;
   private int foo;
   private IList<Element> elements = new List<Element>();

   public Config Name(string name)
   {
      this.name = name;
      return this;
   }

   public Config Foo(int x)
   {
       this.foo = x;
   }

   ... //add method for adding elements 

   class Element
   {
      public string Name { get; set; }
      public int Height { get; set; }
   }
}

有人知道怎么做吗?

【问题讨论】:

  • 查克·诺里斯,人们会期望你回旋解决问题,它会自行解决。 :-)
  • 您已经评论了除我之外的所有答案 - 对此有何感想?如果您真的热衷于手动执行此操作,我可能会提出更多想法 - 但我仍然认为使用对象/集合初始化器是一种更惯用的方法。
  • @JonSkeet 我想知道如何做我在问题顶部展示的内容,我知道这是可能的,你可以在这里看到它demos.telerik.com/aspnet-mvc/grid(查看标签)
  • @ChuckNorris:但是为什么您想避免使用惯用的 C# 方法吗?我会发现你提出的代码比集合/对象初始化方法更难理解。在这里引入 lambda 表达式有什么好处?仅仅因为别人的 API 使用了一个想法并不意味着它是一个的想法......
  • @JonSkeet 一个好处是您可以使配置类通用 并且为 T 的每个属性强类型化元素

标签: c# .net fluent-interface


【解决方案1】:
public class Config
{
   private string name;
   private IList<Element> elements = new List<Element>();
   public IList<Element> GetElements {get {return this.elements;}}
   public Config Name(string name)
   {
      this.name = name;
      return this;
   }

   public Config Elements(IEnumerable<Element> list)
   {
        foreach ( var element in list)
            elements.Add(element);
        return this;
   }

   public Config Elements(params Element[] list)
   {
        foreach ( var element in list)
            elements.Add(element);
        return this;
   }

   public Config Elements(params Expression<Func<Element>>[] funcs)
   {
        foreach (var func in funcs )
            elements.Add(func.Compile()());
        return this;
   }

   public Config Elements(params Expression<Func<IEnumerable<Element>>>[] funcs)
   {
        foreach (var func in funcs )
            foreach ( var element in func.Compile()())
                elements.Add(element);
        return this;
   }

   public class Element
   {
      public string Name { get; set; }
      public int Height { get; set; }     
      public Element() {}
      public Element(string name)
      {
         this.Name = name;
      }  
      public Element AddHeight(int height)
      {
          this.Height = height;
          return this;
      }
      public static Element AddName(string name)
      {
        return new Element(name);
      }
   }
}

用法

var cfg = new Config()
    .Name("X")
    .Elements(new [] { new Config.Element { Name = "", Height = 0} })
    .Elements(
            Config.Element.AddName("1").AddHeight(1), 
            Config.Element.AddName("2").AddHeight(2) 
            )
    .Elements(
        () => Config.Element.AddName("1").AddHeight(1)
    )
    .Elements(
        () => new[] {
                Config.Element.AddName("1").AddHeight(1),
                Config.Element.AddName("1").AddHeight(1)
               }
    )

【讨论】:

  • 你也可以使用params关键字。
  • 这是一个有效的解决方案 :) 但我想知道如何用花哨的 lambda 东西来做到这一点
  • 它仍然有新的 [],我想知道如何做 lambda,我在问题中展示的方式
  • 很遗憾,您不能在函数内部使用 yield 语句(yield elem1; yield elemn2 等)。
【解决方案2】:

这是一个与您的第二个代码示例完全一致的版本。虽然它真的很丑 - 我绝对不想自己使用它。文末附注。

using System;
using System.Collections.Generic;

public class Config
{
    private string name;
    private int foo;
    private IList<Element> elements = new List<Element>();

    public Config Name(string name)
    {
        this.name = name;
        return this;
    }

    public Config Foo(int x)
    {
        this.foo = x;
        return this;
    }

    public Config Elements(Action<ElementBuilder> builderAction)
    {
        ElementBuilder builder = new ElementBuilder(this);
        builderAction(builder);
        return this;
    }

    public class ElementBuilder
    {
        private readonly Config config;

        internal ElementBuilder(Config config)
        {
            this.config = config;
        }

        public ElementHeightBuilder Name(string name)
        {
            Element element = new Element { Name = name };
            config.elements.Add(element);
            return new ElementHeightBuilder(element);
        }
    }

    public class ElementHeightBuilder
    {
        private readonly Element element;

        internal ElementHeightBuilder(Element element)
        {
            this.element = element;
        }

        public void Height(int height)
        {
            element.Height = height;
        }
    }

    public class Element
    {
        public string Name { get; set; }
        public int Height { get; set; }
    }
}



class Test
{
    static void Main()
    {
        Config config = new Config();

        config.Name("Foo")
            .Elements(e => {
                e.Name("element1").Height(23);
                e.Name("element2").Height(31);
            })
            .Foo(3232);
    }
}

注意事项:

使用此代码,您必须首先调用Name,然后可以选择为每个元素调用Height - 尽管如果您未能调用Height,则不会有任何抱怨。如果您将 Elements 调用更改为:

  .Elements(e => {
                     e.NewElement().Name("element1").Height(23);
                     e.NewElement().Name("element2").Height(31);
                 })

或者这个:

  .Elements(e => {
                     e.Name("element1").Height(23).AddToConfig();
                     e.Name("element2").Height(31).AddToConfig();
                 })

那么你最终会得到一个更灵活的情况;你可以有一个单独的ElementBuilder 类来做正确的事情。第一个版本的 IMO 更好。

所有这些仍然大大不如我的其他答案中显示的简单有效的对象/集合初始化程序那么愉快,我强烈建议您使用。我对这种方法真的没有任何好处——如果你没有在 Telerik API 中看到过,你自然会想要这个吗?从其他 cmets 看来,您似乎被使用 lambda 表达式的“闪亮”所吸引……不要。它们在正确的环境中很棒,但在我看来,没有它们也有更清洁的方法可以实现这一目标。

我建议您退后一步,弄清楚您是否真的从您最初想要使用的语法中获得任何东西,并考虑您是否愿意在此答案中保留这种代码,或对象/集合初始化程序解决方案中的代码。

编辑:这是我对 Zoltar 建议的解释,它消除了对额外课程的需要:

using System;
using System.Collections.Generic;

public class Config
{
    private string name;
    private int foo;
    private IList<Element> elements = new List<Element>();

    public Config Name(string name)
    {
        this.name = name;
        return this;
    }

    public Config Foo(int x)
    {
        this.foo = x;
        return this;
    }

    public Config Elements(Action<ElementBuilder> builderAction)
    {
        ElementBuilder builder = new ElementBuilder(this);
        builderAction(builder);
        return this;
    }

    public class ElementBuilder
    {
        private readonly Config config;
        private readonly Element element;

        // Constructor called from Elements...
        internal ElementBuilder(Config config)
        {
            this.config = config;
            this.element = null;
        }

        // Constructor called from each method below
        internal ElementBuilder(Element element)
        {
            this.config = null;
            this.element = element;
        }

        public ElementBuilder Name(string name)
        {
            return Mutate(e => e.Name = name);
        }

        public ElementBuilder Height(int height)
        {
            return Mutate(e => e.Height = height);
        }

        // Convenience method to avoid repeating the logic for each
        // property-setting method
        private ElementBuilder Mutate(Action<Element> mutation)
        {
            // First mutation call: create a new element, return
            // a new builder containing it.
            if (element == null)
            {
                Element newElement = new Element();
                config.elements.Add(newElement);
                mutation(newElement);
                return new ElementBuilder(newElement);
            }
            // Subsequent mutation: just mutate the element, return
            // the existing builder
            mutation(element);
            return this;
        }
    }

    public class Element
    {
        public string Name { get; set; }
        public int Height { get; set; }
    }
}

【讨论】:

  • 我猜你认为这很丑,因为你没有利用 lambda,正如我所说,你可以使 Config 通用(或者可能是 Elements 方法),然后有可能为每个属性做一个元素,例如Elements&lt;Foo&gt; ( els =&gt; {els.Add(o =&gt; o.Prop1).Height(32).Width(22); els =&gt; els.Add(o =&gt; o.Prop2).Color("red").SomeElse(232); }
  • 我也看到你做了一个完整的类只是为了设置 height 属性,这确实很丑陋,我敢肯定 Telerik 不会这样做,如果我需要添加大约 20属性比它会是相当多的代码:)
  • @JonSkeet +1 表示英勇的努力,但同意 Telerik API 示例不值得效仿。您可以通过使用一个类来消除对两个单独的构建器的需求,每个操作都会检查以确保项目已添加到集合中,但这似乎更糟。 Chuck 上面的评论似乎表明他可能会接受将 Add 方法称为链中的第一个方法。
  • @ChuckNorris:这极大地改变了语法(现在是嵌套的 lambdas!),我根本不清楚它甚至意味着要实现什么。你不需要每个属性额外的类 - 它只是因为e 上的第一次调用与所有其他调用不同,因为它添加了一个新元素以及设置一个属性。基本上,您需要 e 与返回到 e.Name() 的类型不同。这就是为什么你必须从 e.Add() 开始更好的原因,正如我所提到的 - 但你说你希望 exact 语法能够工作。
  • @Xoltar:我不确定“检查”是如何工作的——e.Name(...).Height(); e.Name(...).Height(); 的处理方式与e.Name(..).Height(..).Name(..).Height(..) 的处理方式有何不同?您可能需要检查是否已经在该元素上设置了属性,这真的犯规了。呸,呸,呸。
【解决方案3】:

我宁愿使用以下流畅的界面:

Config config = new Config("Foo")
                        .WithElement("element1", 23)
                        .WithElement("element2");

我认为它更具可读性和紧凑性。实施:

public class Config
{
    private string name;
    private IList<Element> elements = new List<Element>();

    public Config(string name)
    {
        this.name = name;
    }

    public Config WithElement(string name, int height = 0)
    {
        elements.Add(new Element() { Name = name, Height = height });
        return this;
    }

    class Element
    {
        public string Name { get; set; }
        public int Height { get; set; }
    }
}

如果 name 是可选的,则添加不带参数的 Config 构造函数。如果您不需要同时使用高度和名称,也请考虑 WithElemnt 方法的可选参数。

更新:我将高度更改为可选参数,以显示如何添加仅指定名称的元素。

更新(如果您只想允许一组元素)

Config config = new List<Element>()
                    .AddElement(new Element {Name = "element1", Height = 23 })
                    .AddElement(new Element {Name = "element2" })
                    .WrapToConfig()
                    .Name("config1");

实施:

public static class ConfigurationHelper
{
    public static IList<Element> AddElement(this IList<Element> elements, Element element)
    {
        elements.Add(element);
        return elements;
    }

    public static Config WrapToConfig(this IList<Element> elements)
    {
        return Config(elements);
    }
}

但这对用户来说不是很明显,所以我会使用第一个简单流畅的界面。

【讨论】:

  • 同意。最终,流畅界面的目的是......流畅。看看我的成绩,已经不流利了。
  • 这实际上是我的第一个解决方案,与这种方法的不同之处在于元素不会一起定义,用户可以执行以下操作:config.WithElement("a").Name( "x").WithElement("xxsa"),你也不能像 @AdrianIftode 的回答那样使用亚流利的 api
  • @ChuckNorris WithElement("a") 已经创建了一个名为“a”的元素。用户不需要使用 Name("x")
  • @AdrianIftode 名称用于配置对象而不是元素
  • @ChuckNorris 为什么不将 Name 作为构造函数参数传递给 Config? WithElement 之间的 Name 有什么问题 - 两者都是 Config 对象设置的一部分。这取决于程序员他将放置这些设置部分的顺序。为什么有人要在元素之间设置名称?
【解决方案4】:

有什么理由不想使用对象和集合初始化器?

public class Config
{
   public string Name { get; set; }
   public int Foo { get; set; }
   public IList<Element> Elements { get; private set; }

   public Config()
   {
       Elements = new List<Element>();
   }
}

// I'm assuming an element *always* needs a name and a height
class Element
{
   public string Name { get; private set; }
   public int Height { get; private set; }

   public Element(string name, int height)
   {
       this.Name = name;
       this.Height = height;
   }
}

然后:

var config = new Config
{
    Name = "Foo",
    Elements = { 
        new Element("element1", 23),
        new Element("element2", 31)
    },
    Foo = 23
};

如果您不想直接公开元素列表,您可以随时将其转换为构建器,并将其复制到Build 上更私密的数据结构中:

var config = new Config.Builder
{
    Name = "Foo",
    Elements = { 
        new Element("element1", 23),
        new Element("element2", 31)
    },
    Foo = 23
}.Build();

这还有一个额外的好处是您可以使Config 本身不可变。

如果您总是需要Name 存在,只需将其作为构造函数参数即可。

虽然有时最好有一个流畅的接口和可变(或复制和更改)方法调用,但在这种情况下,我认为集合/对象初始化器更符合 C# 的习惯。

请注意,如果您使用 C# 4 并且想要调用 Element 构造函数,则始终可以使用命名参数:

new Element(name: "element2", height: 31)

【讨论】:

    【解决方案5】:

    使用数据构建器模式。这样做的好处是它将流畅的构建 api 与数据对象分开。当然,您可以在约定中省略“with”。

    用法:

    var aConfig = new ConfigBuilder();
    
    // create config fluently with lambdas
    Config config = aConfig.WithName("Foo")
            .WithElement(e => e.WithName("element1").WithHeight(23))
            .WithElement(e => e.WithName("element2").WithHeight(31))
        .WithFoo(3232)
        .Build();
    
    // create elements in one go
    config = aConfig.WithName("Foo")
             .WithElements(
                 e => e.WithName("element1").WithHeight(23), 
                 e => e.WithName("element2").WithHeight(31))
         .WithFoo(3232)
         .Build();
    
    
    var anElement = new ElementBuilder();
    
    // or with builders 
    config = aConfig.WithName("Foo")
            .WithElement(anElement.WithName("element1").WithHeight(23))
            .WithElement(anElement.WithName("element2").WithHeight(31))
        .WithFoo(3232)
        .Build();
    
    // use builders to reuse configuration code
    anElement.WithHeigh(100);
    
    config = aConfig.WithName("Bar")
            .WithElement(anElement.WithName("sameheight1"))
            .WithElement(anElement.WithName("sameheight2"))
        .WithFoo(5544)
        .Build();
    

    实施:

    public class ConfigBuilder
    {
        private string name;
        private int foo;
        private List<Element> elements = new List<Element>();
    
        public ConfigBuilder WithName(string name)
        {
             this.name = name;
             return this;
        }
    
        public ConfigBuilder WithFoo(int foo)
        {
            this.foo = foo;
            return this;
        }
    
        public ConfigBuilder WithElement(Element element)
        {
            elements.Add(element);
            return this;
        }
    
        public ConfigBuilder WithElement(ElementBuilder element)
        {
            return WithElement(element.Build());
        }
    
        public ConfigBuilder WithElement(Action<ElementBuilder> builderConfig)
        {
             var elementBuilder = new ElementBuilder();
             builderConfig(elementBuilder);
             return this.WithElement(elementBuilder);
        }
    
        public ConfigBuilder WithElements(params Action<ElementBuilder>[] builderConfigs)
        {
             foreach(var config in builderConfigs)
             {
                  this.WithElement(config);
             }
    
             return this;
        }
    
        public Config Build()
        {
             return new Config() 
             { 
                 Name = this.name,
                 Foo = this.foo,
                 Elements = this.elements
             };
        }
    }
    
    public class ElementBuilder
    {
        private string name;
        private int height;
    
        public ElementBuilder WithName(string name)
        {
            this.name = name;
            return this;
        }
    
        public ElementBuilder WithHeight(int height)
        {
            this.height = height;
            return this;
        }
    
        public Element Build()
        {
            return new Element() 
            { 
                Name = this.name,
                Height = this.height
            };
        }
    }
    
    public class Config
    {
        public string Name { get; set; }
        public int Foo { get; set; }
        public IList<Element> Elements { get; set; }
    }
    
    public class Element
    {
        public string Name { get; set; }
        public int Height { get; set; }
    }
    

    【讨论】:

    • 实现中没有WithElements
    【解决方案6】:

    这是放置在 Config 中的方法 1——“一次一个”:

    public Config Element(Action<Element> a) {
        Element e = new Element();
        a(e);
        this.elements.Add(e);
        return this;
    }
    

    下面是如何使用它:

    config.Name("Foo")
        .Element(e => e.Name("element1").Height(23))
        .Element(e => e.Name("element2").Height(31))
        .Foo(3232);
    

    这里是方法2——“列表”:

    public Config Elements(Func<List<Element>> a) {
        List<Element> elements = a();
        foreach (Element e in elements) {
            this.elements.Add(e);
        }
        return this;
    }
    

    下面是如何使用它:

    config.Name("Foo")
        .Elements(() => new List<Element>() {
                new Element().Name("element1").Height(23),
                new Element().Name("element2").Height(31)
            })
        .Foo(3232);
    

    请注意,它假定 Element 没有嵌套在 Config 中(或者您在示例 2 中需要 new Config.Element())。

    注意,在您的“列表”示例中,您传入了一个 Element 对象,但您尝试设置它两次。第二行将改变元素,而不是创建一个新元素。:

    .Elements(e => {
                     e.Name("element1").Height(23); // <-- You set it
                     e.Name("element2").Height(31); // <-- You change it
                  })
    .Foo(3232);
    

    因此这个语法不能工作。

    工作原理:

    Func&lt;T,U,...&gt; 是一个匿名函数委托,它接受除一个以外的所有参数,并返回最后一个。 Action&lt;T,U,...&gt; 是一个匿名函数委托,它接受所有参数。例如:

    Func&lt;int,string&gt; f = i =&gt; i.ToString(); 说“接受一个 int,返回一个字符串”。

    Action&lt;int&gt; f = i =&gt; string c = i.ToString(); 表示“接受 int,不返回任何内容”。

    【讨论】:

    • "你已经传入了一个 Element 对象,但你试图设置它两次" - 不一定,这就是为什么在第一个示例中我使用 () => ,但在第二个 e 可能意味着一些建设者的事情
    • 如果它是一些构建器,你需要像 e.New().Name(..).Height(..) 这样的东西来表示从“builder”到“instance”的上下文变化否则,您需要 .Name(..) 才能产生创建新名称的神奇副作用。
    猜你喜欢
    • 2018-07-08
    • 1970-01-01
    • 2022-08-19
    • 2010-09-19
    • 2010-09-29
    • 1970-01-01
    • 2010-11-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多