18

“流畅的界面”是近来相当热门的话题。C# 3.0 有一些很好的特性(尤其是扩展方法)可以帮助您制作它们。

仅供参考,流畅的 API 意味着每个方法调用都会返回一些有用的东西,通常是您调用该方法的同一个对象,因此您可以继续链接事物。Martin Fowler 在这里通过一个 Java 示例对其进行了讨论。这个概念是这样的:

var myListOfPeople = new List<Person>();

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

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

所以,我想通过查看和讨论一些优秀的例子来了解更多信息。那么,您在 C# 中制作或见过哪些出色的流畅接口,是什么让它们如此有价值?

谢谢。

4

11 回答 11

9

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

LINQ 背后是一系列方法,其中大部分是扩展方法,它们至少接受一个 IEnumerable 并返回另一个 IEnumerable。这允许非常强大的方法链接

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

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

var array = ImmutableCollection<int>.Empty.Add(42).Add(13).Add(12);
于 2009-03-27T04:40:03.367 回答
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。非常简单,非常可测试,非常好!

于 2009-03-27T17:16:37.543 回答
7

我喜欢CutEdge.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 语句来处理相同的检查时更有效。

于 2009-03-27T17:02:26.360 回答
4

这是我昨天刚做的一个。进一步的思考可能会导致我改变方法,但即使这样,“流利”的方法也让我完成了我原本无法完成的事情。

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

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

请注意,没有包含“lastMonthsInvoice”的字符串,这很好,因为字符串不适合重构。但是,错误消息可能会显示类似“参数 'lastMonthsInvoice' 不能为空”之类的内容。是解释为什么这有效并指向该人的博客文章的帖子。

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

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

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

ConsoleHelper.WriteProperty( ()=> lastMonthsInvoice );

并得到这个输出:

Property [lastMonthsInvoice] is: <whatever ToString from Invoice

生产>

现在,这里是一种流利的方法让我可以做一些我不能做的事情。

我想让 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() { }
}
于 2009-03-27T19:56:49.810 回答
3

除了这里指定的之外,流行的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);
于 2009-03-27T17:28:05.487 回答
3

方法命名

只要明智地选择方法名称,流畅的接口就具有可读性。

考虑到这一点,我想将这个特定的 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# 中并不经常需要它。

由于属性可以具有手动编码的设置器,因此可以在新构造的对象上调用多个方法,而不必让每个方法返回相同的对象。

限制是:

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

如果我们可以在对象初始化程序块内调用方法和登记事件,以及分配给属性,我会很高兴。

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 关键字,它对某种类型的对象进行操作并产生相同的对象和类型 - 请注意,这将是一个表达式,而不是一个语句。所以它会准确地捕捉到在“流利的界面”中链接的想法。

因此,无论您是从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;
}

上面的包装器只是强制一个不返回的动作返回一些东西,嘿,从这个意义上说,任何东西都可以是“流利的”。

等效于初始化程序,但具有事件登记:

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!");
            };

我也忍不住给它“可能是单子”式的 null 检查,所以如果你有一些可能返回的东西null,你仍然可以申请With它,然后检查它的null-ness。

于 2009-03-27T17:40:36.580 回答
2

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

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

tweetsharp也广泛使用了 fluent 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也使用它们,但我无法快速找到示例。

于 2009-03-27T16:43:29.353 回答
2

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

于 2012-08-01T07:49:54.443 回答
1

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

于 2009-03-27T03:27:33.713 回答
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>();
于 2009-03-27T05:18:18.610 回答
0

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

Bind<IWeapon>().To<Sword>();
Bind<Samurai>().ToSelf();
Bind<Shogun>().ToSelf().Using<SingletonBehavior>();
于 2009-03-27T16:58:46.853 回答