您在C#中制作或看到的哪些流畅的界面非常有价值?他们真是太棒了?

时间:2009-03-27 03:22:33

标签: c# c#-3.0 extension-methods fluent

“流畅的界面”如今是一个相当热门的话题。 C#3.0有一些很好的功能(特别是扩展方法)可以帮助你制作它们。

仅供参考,一个流畅的API意味着每个方法调用都返回一些有用的东西,通常是你调用方法的同一个对象,所以你可以继续链接。 Martin Fowler用Java示例here讨论了它。这个概念就像这样:

var myListOfPeople = new List<Person>();

var person = new Person();
person.SetFirstName("Douglas").SetLastName("Adams").SetAge(42).AddToList(myListOfPeople);

我在C#中看到了一些非常有用的流畅接口(一个例子是用于验证an earlier StackOverflow question I had asked中找到的参数的流畅方法。它让我感到震惊。它能够为表达参数验证规则提供高度可读的语法,而且,如果没有例外,它可以避免实例化任何对象!所以对于“正常情况”,开销非常小。这一小窍门教我一个巨大的金额时间。我想找到更多类似的东西。)

所以,我想通过观察和讨论一些优秀的例子来了解更多。那么,您在C#中制作或看到的优秀流畅界面是什么,以及是什么让它们变得如此有价值?

感谢。

11 个答案:

答案 0 :(得分:9)

这实际上是我第一次听到“流畅的界面”这个词。但是我想到的两个例子是LINQ和不可变集合。

在幕后,LINQ是一系列方法,其中大多数是扩展方法,至少需要一个IEnumerable并返回另一个IEnumerable。这允许非常强大的方法链接

var query = someCollection.Where(x => !x.IsBad).Select(x => x.Property1);

不可变类型,更具体地说,集合具有非常相似的模式。 Immutable Collections返回一个新的集合,通常是一个变异操作。因此,构建集合通常会变成一系列链式方法调用。

var array = ImmutableCollection<int>.Empty.Add(42).Add(13).Add(12);

答案 1 :(得分:8)

方法参数验证的荣誉,你给了我一个关于我们流畅的API的新想法。无论如何,我讨厌我们的前提条件检查......

我为开发中的新产品构建了一个可扩展性系统,您可以流畅地描述可用的命令,用户界面元素等。它运行在StructureMap和FluentNHibernate之上,这也是很好的API。

MenuBarController mb;
// ...
mb.Add(Resources.FileMenu, x =>
{
  x.Executes(CommandNames.File);
  x.Menu
    .AddButton(Resources.FileNewCommandImage, Resources.FileNew, Resources.FileNewTip, y => y.Executes(CommandNames.FileNew))
    .AddButton(null, Resources.FileOpen, Resources.FileOpenTip, y => 
    {
      y.Executes(CommandNames.FileOpen);
      y.Menu
        .AddButton(Resources.FileOpenFileCommandImage, Resources.OpenFromFile, Resources.OpenFromFileTop, z => z.Executes(CommandNames.FileOpenFile))
        .AddButton(Resources.FileOpenRecordCommandImage, Resources.OpenRecord, Resources.OpenRecordTip, z => z.Executes(CommandNames.FileOpenRecord));
     })
     .AddSeperator()
     .AddButton(null, Resources.FileClose, Resources.FileCloseTip, y => y.Executes(CommandNames.FileClose))
     .AddSeperator();
     // ...
});

您可以配置所有可用的命令:

Command(CommandNames.File)
  .Is<DummyCommand>()
  .AlwaysEnabled();

Command(CommandNames.FileNew)
  .Bind(Shortcut.CtrlN)
  .Is<FileNewCommand>()
  .Enable(WorkspaceStatusProviderNames.DocumentFactoryRegistered);

Command(CommandNames.FileSave)
  .Bind(Shortcut.CtrlS)
  .Enable(WorkspaceStatusProviderNames.DocumentOpen)
  .Is<FileSaveCommand>();

Command(CommandNames.FileSaveAs)
  .Bind(Shortcut.CtrlShiftS)
  .Enable(WorkspaceStatusProviderNames.DocumentOpen)
  .Is<FileSaveAsCommand>();

Command(CommandNames.FileOpen)
  .Is<FileOpenCommand>()
  .Enable(WorkspaceStatusProviderNames.DocumentFactoryRegistered);

Command(CommandNames.FileOpenFile)
  .Bind(Shortcut.CtrlO)
  .Is<FileOpenFileCommand>()
  .Enable(WorkspaceStatusProviderNames.DocumentFactoryRegistered);

Command(CommandNames.FileOpenRecord)
  .Bind(Shortcut.CtrlShiftO)
  .Is<FileOpenRecordCommand>()
  .Enable(WorkspaceStatusProviderNames.DocumentFactoryRegistered);

我们的视图使用工作区为他们提供的服务配置标准编辑菜单命令的控件,他们只是告诉它观察它们:

Workspace
  .Observe(control1)
  .Observe(control2)

如果用户选中控件,工作区会自动为控件获取适当的适配器,并提供撤消/重做和剪贴板操作。

它帮助我们大幅减少了设置代码,使其更具可读性。


我忘了告诉我们在WinForms MVP模型演示者中使用的库来验证视图:FluentValidation。真的很容易,真的可以测试,非常好!

答案 2 :(得分:7)

我喜欢CuttingEdge.Conditions中的流畅界面。

来自他们的样本:

 // Check all preconditions:
 id.Requires("id")
    .IsNotNull()          // throws ArgumentNullException on failure 
    .IsInRange(1, 999)    // ArgumentOutOfRangeException on failure 
    .IsNotEqualTo(128);   // throws ArgumentException on failure 
 

我发现它更容易阅读,并且使我在检查方法中的前置条件(和后置条件)方面比我有50条if语句来处理相同的检查更有效。

答案 3 :(得分:4)

这是我昨天做的一个。进一步的思考可能会让我改变方法,但即使如此,“流利”的方法让我能够完成我本不可能拥有的东西。

首先,一些背景。我最近学到了(这里是StackOverflow)一种将值传递给方法的方法,这样该方法就可以确定名称。例如,一种常见用途是用于参数验证。例如:

public void SomeMethod(Invoice lastMonthsInvoice)
{
     Helper.MustNotBeNull( ()=> lastMonthsInvoice);
}

请注意,没有包含“lastMonthsInvoice”的字符串,这很好,因为字符串很难进行重构。但是,错误消息可以说“参数'lastMonthsInvoice'不能为空。” Here's the post解释了为什么这有效并指向该人的博客文章。

但这只是背景。我使用相同的概念,但方式不同。我正在编写一些单元测试,并且我想将某些属性值转储到控制台,以便它们显示在单元测试输出中。我厌倦了写这个:

Console.WriteLine("The property 'lastMonthsInvoice' has the value: " + lastMonthsInvoice.ToString());

...因为我必须将属性命名为字符串,然后引用它。所以我把它打到了我可以输入的地方:

ConsoleHelper.WriteProperty( ()=> lastMonthsInvoice );

获得此输出:

Property [lastMonthsInvoice] is: <whatever ToString from Invoice
     

产生&GT;

现在,流利的方法允许我做一些我不能做的事情。

我想让ConsoleHelper.WriteProperty采用params数组,因此它可以将许多这样的属性值转储到控制台。要做到这一点,它的签名将如下所示:

public static void WriteProperty<T>(params Expression<Func<T>>[] expr)

所以我可以这样做:

ConsoleHelper.WriteProperty( ()=> lastMonthsInvoice, ()=> firstName, ()=> lastName );

但是,由于类型推断而无效的换句话说,所有这些表达式都不会返回相同的类型。 lastMonthsInvoice是一个发票。 firstName和lastName是字符串。它们不能在对WriteProperty的同一调用中使用,因为T在所有这些调用中都不相同。

这是流畅的方法来拯救的地方。我让WriteProperty()返回一些东西。它返回的类型是我可以调用And()的类型。这给了我这样的语法:

ConsoleHelper.WriteProperty( ()=> lastMonthsInvoice)
     .And( ()=> firstName)
     .And( ()=> lastName);

这种情况下,流畅的方法允许一些本来不可能的东西(或至少不方便)。

这是完整的实施。正如我所说,我昨天写了。您可能会看到改进的空间,甚至可能是更好的方法。我很欢迎。

public static class ConsoleHelper
{
    // code where idea came from ...
    //public static void IsNotNull<T>(Expression<Func<T>> expr)
    //{
    // // expression value != default of T
    // if (!expr.Compile()().Equals(default(T)))
    // return;

    // var param = (MemberExpression)expr.Body;
    // throw new ArgumentNullException(param.Member.Name);
    //}

    public static PropertyWriter WriteProperty<T>(Expression<Func<T>> expr)
    {
        var param = (MemberExpression)expr.Body;
        Console.WriteLine("Property [" + param.Member.Name + "] = " + expr.Compile()());
        return null;
    }

    public static PropertyWriter And<T>(this PropertyWriter ignored, Expression<Func<T>> expr)
    {
        ConsoleHelper.WriteProperty(expr);
        return null;
    }

    public static void Blank(this PropertyWriter ignored)
    {
        Console.WriteLine();
    }
}

public class PropertyWriter
{
    /// <summary>
    /// It is not even possible to instantiate this class. It exists solely for hanging extension methods off.
    /// </summary>
    private PropertyWriter() { }
}

答案 4 :(得分:3)

除了这里指定的内容之外,popuplar RhinoMocks单元测试模拟框架使用流畅的语法来指定对模拟对象的期望:

// Expect mock.FooBar method to be called with any paramter and have it invoke some method
Expect.Call(() => mock.FooBar(null))
    .IgnoreArguments()
    .WhenCalled(someCallbackHere);

// Tell mock.Baz property to return 5:
SetupResult.For(mock.Baz).Return(5);

答案 5 :(得分:3)

方法命名

只要合理选择方法名称,Fluent界面就可以提供可读性。

考虑到这一点,我想提名这个特殊的API为“反流畅”:

System.Type.IsInstanceOfType

它是System.Type的成员并接受一个对象,如果该对象是该类型的实例,则返回true。不幸的是,你自然倾向于从左到右阅读它:

o.IsInstanceOfType(t);  // wrong

当它实际上是另一种方式时:

t.IsInstanceOfType(o);  // right, but counter-intuitive

但并非所有方法都可能被命名(或定位在BCL中)以预测它们如何出现在“伪英语”代码中,因此这不是真正的批评。我只是指出了流畅接口的另一个方面 - 选择方法名称以引起最少的惊喜。

对象初始值设定项

这里给出了很多例子,使用流畅接口的唯一原因是,可以在一个表达式中初始化新分配对象的几个属性。

但是C#有一个语言功能,经常使这个不必要的 - 对象初始化语法:

var myObj = new MyClass
            {
                SomeProperty = 5,
                Another = true,
                Complain = str => MessageBox.Show(str),
            };

这或许可以解释为什么专家C#用户不太熟悉用于链接同一对象调用的“流畅接口”这个术语 - 在C#中不需要经常使用它。

由于属性可以有手动编码的setter,这是一个在新构造的对象上调用多个方法的机会,而不必让每个方法都返回相同的对象。

限制是:

  • 属性设置器只能接受一个参数
  • 属性设置器不能是通用的

如果我们可以调用方法并在事件中登记,以及在对象初始化程序块内分配属性,我希望如此。

var myObj = new MyClass
            {
                SomeProperty = 5,
                Another = true,
                Complain = str => MessageBox.Show(str),
                DoSomething()
                Click += (se, ev) => MessageBox.Show("Clicked!"),
            };

为什么这样的修改只能在施工后立即适用?我们可以:

myObj with
{
    SomeProperty = 5,
    Another = true,
    Complain = str => MessageBox.Show(str),
    DoSomething(),
    Click += (se, ev) => MessageBox.Show("Clicked!"),
}

with将是一个新的关键字,对某种类型的对象进行操作并生成相同的对象和类型 - 请注意,这将是表达式,而不是声明。因此,它将完全捕捉到在“流畅的界面”中链接的想法。

因此,无论您是从new表达式还是从IOC或工厂方法获取对象,都可以使用初始化器样式语法。

事实上,你可以在完成with之后使用new,它将等同于对象初始值设定项的当前样式:

var myObj = new MyClass() with
            {
                SomeProperty = 5,
                Another = true,
                Complain = str => MessageBox.Show(str),
                DoSomething(),
                Click += (se, ev) => MessageBox.Show("Clicked!"),
            };

正如查理在评论中指出的那样:

public static T With(this T with, Action<T> action)
{
    if (with != null)
        action(with);
    return with;
}

上面的包装器只是强制一个不返回的动作返回一些东西,而且嘿presto-在这个意义上任何东西都可以“流畅”。

相当于初始化程序,但事件征集:

var myObj = new MyClass().With(w =>
            {
                w.SomeProperty = 5;
                w.Another = true;
                w.Click += (se, ev) => MessageBox.Show("Clicked!");
            };

使用工厂方法而不是new

var myObj = Factory.Alloc().With(w =>
            {
                w.SomeProperty = 5;
                w.Another = true;
                w.Click += (se, ev) => MessageBox.Show("Clicked!");
            };

我无法抗拒给它“也许monad”式的null检查,所以如果你有可能返回null的东西,你仍然可以将With应用于它,然后检查null - ness。

答案 6 :(得分:2)

SubSonic 2.1对查询API有一个不错的选择:

DB.Select()
  .From<User>()
  .Where(User.UserIdColumn).IsEqualTo(1)
  .ExecuteSingle<User>();

tweetsharp也广泛使用了流畅的API:

var twitter = FluentTwitter.CreateRequest()
              .Configuration.CacheUntil(2.Minutes().FromNow())
              .Statuses().OnPublicTimeline().AsJson();

Fluent NHibernate最近风靡一时:

public class CatMap : ClassMap<Cat>  
{  
  public CatMap()  
  {  
    Id(x => x.Id);  
    Map(x => x.Name)  
      .WithLengthOf(16)  
      .Not.Nullable();  
    Map(x => x.Sex);  
    References(x => x.Mate);  
    HasMany(x => x.Kittens);  
  }  
}  

Ninject也使用它们,但我找不到快速的例子。

答案 7 :(得分:1)

WCF REST Starter Kit Preview 2的新HttpClient是一个非常流畅的API。请参阅我的博文,了解示例http://bendewey.wordpress.com/2009/03/14/connecting-to-live-search-using-the-httpclient/

答案 8 :(得分:1)

NHibernate中的Criteria API有一个很好的流畅界面,可以让你做这样的酷事:

Session.CreateCriteria(typeof(Entity))
    .Add(Restrictions.Eq("EntityId", entityId))
    .CreateAlias("Address", "Address")
    .Add(Restrictions.Le("Address.StartDate", effectiveDate))
    .Add(Restrictions.Disjunction()
        .Add(Restrictions.IsNull("Address.EndDate"))
        .Add(Restrictions.Ge("Address.EndDate", effectiveDate)))
    .UniqueResult<Entity>();

答案 9 :(得分:1)

我为System.Net.Mail编写了一个流畅的包装器,我发现它使电子邮件代码更具可读性(并且更容易记住语法)。

var email = Email
            .From("john@email.com")
            .To("bob@email.com", "bob")
            .Subject("hows it going bob")
            .Body("yo dawg, sup?");

//send normally
email.Send();

//send asynchronously
email.SendAsync(MailDeliveredCallback);

http://lukencode.com/2010/04/11/fluent-email-in-net/

答案 10 :(得分:0)

提到@ John Sheehan时,Ninject使用此类型的API来指定绑定。以下是来自user guide的一些示例代码:

Bind<IWeapon>().To<Sword>();
Bind<Samurai>().ToSelf();
Bind<Shogun>().ToSelf().Using<SingletonBehavior>();