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

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

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

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

我在C#中看到了一些非常有用的流畅接口(一个例子是用于validation我在之前提出的早期StackOverflow问题中找到的参数的流畅方法。它让我感到震惊。它能够为表达参数validation规则提供高度可读的语法,并且另外,如果没有例外,它可以避免实例化任何对象!所以对于“正常情况”,开销非常小。这一小窍门在短时间内教会了我很多 。我想找到更多的东西像那样)。

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

谢谢。

对于方法参数validation的荣誉,您已经为我们的流畅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() .AlwaysEnabled(); Command(CommandNames.FileNew) .Bind(Shortcut.CtrlN) .Is() .Enable(WorkspaceStatusProviderNames.DocumentFactoryRegistered); Command(CommandNames.FileSave) .Bind(Shortcut.CtrlS) .Enable(WorkspaceStatusProviderNames.DocumentOpen) .Is(); Command(CommandNames.FileSaveAs) .Bind(Shortcut.CtrlShiftS) .Enable(WorkspaceStatusProviderNames.DocumentOpen) .Is(); Command(CommandNames.FileOpen) .Is() .Enable(WorkspaceStatusProviderNames.DocumentFactoryRegistered); Command(CommandNames.FileOpenFile) .Bind(Shortcut.CtrlO) .Is() .Enable(WorkspaceStatusProviderNames.DocumentFactoryRegistered); Command(CommandNames.FileOpenRecord) .Bind(Shortcut.CtrlShiftO) .Is() .Enable(WorkspaceStatusProviderNames.DocumentFactoryRegistered); 

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

 Workspace .Observe(control1) .Observe(control2) 

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

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


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

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

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

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

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

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

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

从他们的样本:

  //检查所有前提条件:
  id.Requires( “ID”)
     .IsNotNull()//失败时抛出ArgumentNullException 
     .IsInRange(1,999)//失败时ArgumentOutOfRangeException 
     .IsNotEqualTo(128);  //失败时抛出ArgumentException 
 

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

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

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

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

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

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

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

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

 ConsoleHelper.WriteProperty( ()=> lastMonthsInvoice ); 

得到这个输出:

 Property [lastMonthsInvoice] is:  

生产>

现在,这里有一个流畅的方法允许我做一些我不能做的事情。

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

 public static void WriteProperty(params Expression>[] 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(Expression> 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(Expression> expr) { var param = (MemberExpression)expr.Body; Console.WriteLine("Property [" + param.Member.Name + "] = " + expr.Compile()()); return null; } public static PropertyWriter And(this PropertyWriter ignored, Expression> expr) { ConsoleHelper.WriteProperty(expr); return null; } public static void Blank(this PropertyWriter ignored) { Console.WriteLine(); } } public class PropertyWriter { ///  /// It is not even possible to instantiate this class. It exists solely for hanging extension methods off. ///  private PropertyWriter() { } } 

除了这里指定的,弹出式RhinoMocksunit testing模拟框架使用流畅的语法来指定对模拟对象的期望:

 // 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); 

SubSonic 2.1适用于查询API:

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

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

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

流畅的NHibernate最近风靡一时:

 public class CatMap : ClassMap { 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也使用它们,但我很快就找不到一个例子。

方法命名

只要方法名称被明智地选择,流畅的界面就有助于提高可读性。

考虑到这一点,我想提名这个特殊的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或工厂方法获取对象,都可以使用初始化器样式语法。

事实上,你可以在完成一个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 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 ,你仍然可以应用它,然后检查它是否为null

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

WCF REST入门套件预览版2的新HttpClient是一个非常流畅的API。 请参阅我的博客文章,了解示例http://bendewey.wordpress.com/2009/03/14/connecting-to-live-search-using-the-httpclient/

我为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/

正如@ John Sheehan所提到的, Ninject使用这种类型的API来指定绑定。 以下是其用户指南中的一些示例代码:

 Bind().To(); Bind().ToSelf(); Bind().ToSelf().Using();