流畅API一览






4.98/5 (55投票s)
对流畅 API 的介绍以及一个示例。
引言
最近,使用流畅 API 的人越来越多,你几乎在哪里都能看到它们,但这些流畅 API 究竟是什么?我哪里可以弄到一个呢?
这是我们的朋友维基百科对此事的说法:
在软件工程中,流畅界面(最初由 Eric Evans 和 Martin Fowler 创造)是一种实现面向对象 API 的方式,旨在使代码更具可读性。流畅界面通常通过使用方法链来传递后续调用的指令上下文(但流畅界面不仅仅是方法链)。通常,上下文通过调用方法的返回值来定义,该方法返回自身,新的上下文等同于通过返回 void 上下文而终止的最后一个上下文。
这种风格因其能够为代码提供更流畅的感受而在可读性方面有轻微的好处,但可能对调试极为不利,因为流畅链构成了一个单一的语句,例如调试器可能不允许设置中间断点。
-- http://en.wikipedia.org/wiki/Fluent_interface 更新于 2011/01/14
本文将讨论现有的各种流畅 API 类型,还将向您展示一个包含我自己创建的流畅 API 的演示应用程序,并讨论在创建自己的流畅 API 时可能遇到的某些问题。
我应该提到,这篇文章非常简单(这对我来说是个例外),我不指望会有很多人喜欢它,但我觉得它可能会帮助到一些人,所以还是发布了。所以,如果你读完后觉得 Sacha,这太糟糕了,请回想一下我在这段文字中告诉你的,这将是一篇非常简单的文章。
相信我,接下来的文章(正在进行中)没那么容易,而且很难消化,所以也许这篇小文章是个不错的开端。
流畅与否
使用流畅 API 的主要原因之一是(如果设计得当)它们遵循我们使用的相同自然语言规则,因此使用起来更容易。我个人觉得它们更容易阅读,而且当我阅读流畅 API 时,整体结构似乎能更好地显现出来。
话虽如此,所有 API 都应该是流畅的吗?当然不是,有些 API 会很糟糕(太大,相互依赖性太多),而且我们不要忘记,流畅 API 需要花费更多时间来开发,而且可能不容易想到,而且你现有的类/方法可能不适合创建流畅 API,除非你一开始就打算创建它。这些是你必须考虑的因素。
看看一些流畅 API 的例子
市面上有很多流畅界面(正如我所说,它们现在非常流行)。我选择了下面概述的两个。我之所以选择这两个,是为了讨论你可能会遇到的不同类型的流畅 API。
讨论点 1:Fluent NHibernate
NHibernate 是 .NET 中一个成熟的对象关系映射器(ORM)。你通常会有一个代码文件,比如说 C#,并且你通常会配置一个 NHibernate 映射,用于你希望持久化的类,使用一个 NHibernate 映射 XML 文件,例如
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
namespace="QuickStart" assembly="QuickStart">
<class name="Cat" table="Cat">
<id name="Id">
<generator class="identity" />
</id>
<property name="Name">
<column name="Name" length="16" not-null="true" />
</property>
<property name="Sex" />
<many-to-one name="Mate" />
<bag name="Kittens">
<key column="mother_id" />
<one-to-many class="Cat" />
</bag>
</class>
</hibernate-mapping>
对于你希望持久化的每个 .NET 类,你都需要生成这些 XML 文件中的一种。有些人想,嘿,为什么不设计一个漂亮的流畅 API 来做同样的事情呢?于是Fluent NHibernate 就诞生了。
下面是一个我们如何Fluent NHibernate 来配置 Cat
类型映射的例子。
public class CatMap : ClassMap<Cat>
{
public CatMap()
{
Id(x => x.Id);
Map(x => x.Name)
.Length(16)
.Not.Nullable();
Map(x => x.Sex);
References(x => x.Mate);
HasMany(x => x.Kittens);
}
}
这里可能不明显的一点是,我们没有返回任何值,也没有启动任何东西,顺序似乎也不太重要;看起来我们可以随意改变流畅 API 的顺序(尽管不推荐)。
Fluent NHibernate 只是配置某些东西,所以顺序可能不一定重要。但是,也存在一些流畅 API,其中流畅 API 项的应用顺序很重要,因为我们可能正在启动依赖于先前流畅 API 项或甚至返回值的内容。
接下来我们将检查一个启动内容的流畅 API,因此流畅 API 项的顺序至关重要。
讨论点 2:NServiceBus 总线配置
另一个例子是 NServiceBus 的总线配置,如下所示:
Bus = NServiceBus.Configure.With()
.DefaultBuilder()
.XmlSerializer()
.RijndaelEncryptionService()
.MsmqTransport()
.IsTransactional(false)
.PurgeOnStartup(true)
.UnicastBus()
.ImpersonateSender(false)
.LoadMessageHandlers() // need this to load MessageHandlers
.CreateBus()
.Start();
注意:NServiceBus 流畅 API 假设你**总是**会以调用 Start()
方法结束,因为它实际上正在启动一个依赖于先前流畅 API 项设置值的内部对象。
我包含的演示应用程序返回一个值,因此可以看作与 NServiceBus 流畅 API 类似;顺序很重要,但我还将向你展示,如果用户没有按正确的顺序提供流畅 API 项,我会如何处理(尽管我的修复非常特定于演示应用程序,但你仍然应该能够看到如何将此逻辑应用于你自己的流畅 API)。
演示项目及其流畅 API
对于附加的演示应用程序,我必须想出一些东西来编写流畅 API。我最终选择了一个非常简单的东西,任何 WPF 开发人员都曾经做过不止一次。那么我选择了什么来探讨呢?
很简单,我围绕获取一组可在后台 TPL 任务中获取的虚拟数据编写了一个故意简单的流畅 API,它允许指定分组和排序,并返回一个 ICollectionView
,该 ICollectionView
用于演示应用程序的 MainWindow
使用的一个非常简单的 MainWindowViewModel
。
正如我所说,我故意着手创建一个**非常简单**的流畅 API,这样人们就能看到这个概念,它可以更优雅,但我希望过度简化它,以便人们可以看到如何构建自己的流畅 API。
这是附加演示代码运行的截图。
让我们来看看下面显示的 MainWindowViewModel
代码。这段代码中最相关的内容是三个 ICommand.Execute()
方法以及它们使用的私有辅助方法。
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Diagnostics;
using FluentDemo.Data.Common;
using FluentDemo.Providers;
using FluentDemo.Model;
using FluentDemo.Commands;
using System.Windows.Threading;
using System.Windows.Data;
namespace FluentDemo.ViewModels
{
public class MainWindowViewModel : INPCBase
{
private ICollectionView demoData;
public MainWindowViewModel()
{
ViewLoadedCommand =
new SimpleCommand<object, object>(ExecuteViewLoadedCommand);
PopulateAsyncCommand =
new SimpleCommand<object, object>(ExecutePopulateAsyncCommand);
PopulateSyncCommand =
new SimpleCommand<object, object>(ExecutePopulateSyncCommand);
InCorrectFluentAPIOrderCommand =
new SimpleCommand<object, object>(
ExecuteInCorrectFluentAPIOrderCommand);
}
private void SetDemoData(ICollectionView icv)
{
DemoData = icv;
}
public SimpleCommand<object, object> ViewLoadedCommand { get; private set; }
public SimpleCommand<object, object> PopulateAsyncCommand { get; private set; }
public SimpleCommand<object, object> PopulateSyncCommand { get; private set; }
public SimpleCommand<object, object>
InCorrectFluentAPIOrderCommand { get; private set; }
public ICollectionView DemoData
{
get
{
return demoData;
}
set
{
if (demoData != value)
{
demoData = value;
NotifyPropertyChanged(new PropertyChangedEventArgs("DemoData"));
}
}
}
private void ExecuteViewLoadedCommand(object args)
{
PopulateSync();
}
private void ExecutePopulateAsyncCommand(object args)
{
PopulateAsync();
}
private void ExecutePopulateSyncCommand(object args)
{
PopulateSync();
}
private void ExecuteInCorrectFluentAPIOrderCommand(object args)
{
//NON-Threaded Version, with incorrect Fluent API ordering
//Oh no, so how do we apply our sorting/grouping, we have missed
//our opportunity, as when we call Run() we get a ICollectionView
DemoData = new DummyModelDataProvider()
//this actually returns ICollectionView,
//so we are effectively at end of Fluent API calls
.Run()
//But help is at hand, with some clever
//extension methods on ICollectionView, we can preempt
//the user doing this, and still get things
//to work, and make it look like a fluent API
.SortBy(x => x.LName, ListSortDirection.Ascending)
.GroupBy(x => x.Location);
}
private void PopulateAsync()
{
//Threaded Version, with correct Fluent API ordering
new DummyModelDataProvider()
.IsThreaded()
.SortBy(x => x.LName, ListSortDirection.Descending)
.GroupBy(x => x.Location)
.RunThreadedWithCallback(SetDemoData);
}
private void PopulateSync()
{
//NON-Threaded Version, with correct Fluent API ordering
DemoData = new DummyModelDataProvider()
.SortBy(x => x.LName, ListSortDirection.Descending)
.GroupBy(x => x.Location)
.Run();
}
}
}
在上面的私有方法中可以看到简单的流畅 API 在起作用。让我们来详细讨论这三个示例中最复杂的一个,然后再看看本文的简单流畅 API。PopulateAsync()
方法最复杂,如下所示:
//Threaded Version, with correct Fluent API ordering
new DummyModelDataProvider()
.IsThreaded()
.SortBy(x => x.LName, ListSortDirection.Descending)
.GroupBy(x => x.Location)
.RunThreadedWithCallback(SetDemoData);
那是什么意思呢?嗯,有几点:
- 我们声明我们希望
DummyModelDataProvider
在线程中运行,所以我们可以合理地假设它将在后台运行。 - 我们指定我们希望使用
DummyModelDataProvider
获取数据的LName
字段对结果进行排序。 - 我们指定我们希望使用
DummyModelDataProvider
获取数据的Location
字段对结果进行分组。 - 我们还提供了一个回调,用于在线程化操作完成时调用。
我认为这样读起来相当不错。
但有一点要注意,就像我们前面看到的 NServiceBus 示例一样,顺序很重要。如果首先调用 RunThreadedWithCallback(..)
方法,则无法使用其他流畅 API 项。或者,如果我们先调用 RunThreadedWithCallback(..)
方法,然后再调用 IsThreaded(..)
方法,它就会失败,因为代码还不知道它必须在后台运行。我知道 IsThreaded(..)
方法实际上是多余的;如果我们处于 RunThreadedWithCallback(..)
方法中,我们可以推断代码应该是线程化的,但正如我所说,这个例子是为了说明流畅 API 的危险/优点而设计的,所以我把它做得完全愚蠢,并把这个多余的 IsThreaded(..)
方法放在那里。
那么,让我们来看看这个演示应用程序的流畅 API 吧(正如 A-Team 的 Mr. T 会说的,“少废话,傻瓜”)。
嗯,这就是全部了,这是完整的代码。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using FluentDemo.Data.Common;
using System.Linq.Expressions;
using FluentDemo.Model;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Data;
namespace FluentDemo.Providers
{
public class SearchResult<T>
{
readonly T package;
readonly Exception error;
public T Package { get { return package; } }
public Exception Error { get { return error; } }
public SearchResult(T package, Exception error)
{
this.package = package;
this.error = error;
}
}
public class DummyModelDataProvider
{
#region Data
private enum SortOrGroup { Sort=1, Group};
private bool isThreaded = false;
private string sortDescription = string.Empty;
private string groupDescription = string.Empty;
private ListSortDirection sortDirection =
ListSortDirection.Ascending;
#endregion
#region Fluent interface
public DummyModelDataProvider IsThreaded()
{
isThreaded = true;
return this;
}
/// <summary>
/// SortBy
/// </summary>
public DummyModelDataProvider SortBy(
Expression<Func<DummyModel, Object>> sortExpression,
ListSortDirection sortDirection)
{
this.sortDescription =
ObjectHelper.GetPropertyName(sortExpression);
this.sortDirection = sortDirection;
return this;
}
/// <summary>
/// GroupBy
/// </summary>
public DummyModelDataProvider GroupBy(
Expression<Func<DummyModel, Object>> groupExpression)
{
this.groupDescription =
ObjectHelper.GetPropertyName(groupExpression);
return this;
}
public ICollectionView Run()
{
ICollectionView collectionView =
CollectionViewSource.GetDefaultView(GetItems(false));
collectionView = ApplySortOrGroup(
collectionView, SortOrGroup.Sort, sortDescription);
collectionView = ApplySortOrGroup(
collectionView, SortOrGroup.Group, groupDescription);
return collectionView;
}
public void RunThreadedWithCallback(
Action<ICollectionView> threadCallBack)
{
ICollectionView collectionView = null;
if (threadCallBack == null)
throw new ApplicationException("threadCallBack can not be null");
GetAll(
(data) =>
{
if (data != null)
{
collectionView = CollectionViewSource.GetDefaultView(data);
collectionView = ApplySortOrGroup(
collectionView, SortOrGroup.Sort, sortDescription);
collectionView = ApplySortOrGroup(
collectionView, SortOrGroup.Group, groupDescription);
}
threadCallBack(collectionView);
},
(ex) =>
{
throw ex;
});
}
#endregion
#region Private Methods
private ICollectionView ApplySortOrGroup(ICollectionView icv,
SortOrGroup operation, string val)
{
if (string.IsNullOrEmpty(val))
return icv;
using (icv.DeferRefresh())
{
icv.GroupDescriptions.Clear();
icv.SortDescriptions.Clear();
switch (operation)
{
case SortOrGroup.Sort:
icv.SortDescriptions.Add(
new SortDescription(val, sortDirection));
break;
case SortOrGroup.Group:
icv.GroupDescriptions.Add(
new PropertyGroupDescription(val, null,
StringComparison.InvariantCultureIgnoreCase));
break;
}
}
return icv;
}
//This is obviously just a simulated list, this would come from Web Service
//or whatever source your data comes from
private List<DummyModel> GetItems(bool isAsync)
{
List<DummyModel> items = new List<DummyModel>();
items.Add(new DummyModel("UK","sacha","barber",
isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
items.Add(new DummyModel("UK", "sacha", "distell",
isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
items.Add(new DummyModel("Greece","sam","bard",
isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
items.Add(new DummyModel("Brazil","sarah","burns",
isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
items.Add(new DummyModel("Australia", "gabriel","barnett",
isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
items.Add(new DummyModel("Australia", "gabe", "burns",
isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
items.Add(new DummyModel("Ireland", "hale","yeds",
isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
items.Add(new DummyModel("New Zealand", "harlen","frets",
isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
items.Add(new DummyModel("Australia", "ryan", "oberon",
isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
items.Add(new DummyModel("Australia", "tim", "meadows",
isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
items.Add(new DummyModel("Thailand", "dwayne", "zarconi",
isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
//add a few more if being called in async mode
//just so user sees a change in the UI
if (isAsync)
{
items.Add(new DummyModel("Australia", "elvis", "maandrake",
isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
items.Add(new DummyModel("Australia", "tony", "montana",
isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
items.Add(new DummyModel("Ireland", "esmerelda", "klakenhoffen",
isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
}
return items.ToList();
}
private void GetAll(
Action<IEnumerable<DummyModel>> resultCallback,
Action<Exception> errorCallback)
{
Task<SearchResult<IEnumerable<DummyModel>>> task =
Task.Factory.StartNew(() =>
{
try
{
return new SearchResult<IEnumerable<DummyModel>>(
GetItems(true), null);
}
catch (Exception ex)
{
return new SearchResult<IEnumerable<DummyModel>>(null, ex);
}
});
task.ContinueWith(r =>
{
if (r.Result.Error != null)
{
errorCallback(r.Result.Error);
}
else
{
resultCallback(r.Result.Package);
}
}, CancellationToken.None, TaskContinuationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
}
#endregion
}
}
这看起来可能不太好,但如果我们只显示流畅 API,看看我们得到了什么:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using FluentDemo.Data.Common;
using System.Linq.Expressions;
using FluentDemo.Model;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Data;
namespace FluentDemo.Providers
{
public class DummyModelDataProvider
{
#region Data
private enum SortOrGroup { Sort=1, Group};
private bool isThreaded = false;
private string sortDescription = string.Empty;
private string groupDescription = string.Empty;
private ListSortDirection sortDirection = ListSortDirection.Ascending;
#endregion
#region Fluent interface
public DummyModelDataProvider IsThreaded()
{
isThreaded = true;
return this;
}
/// <summary>
/// SortBy
/// </summary>
public DummyModelDataProvider SortBy(
Expression<Func<DummyModel, Object>> sortExpression,
ListSortDirection sortDirection)
{
this.sortDescription =
ObjectHelper.GetPropertyName(sortExpression);
this.sortDirection = sortDirection;
return this;
}
/// <summary>
/// GroupBy
/// </summary>
public DummyModelDataProvider GroupBy(
Expression<Func<DummyModel, Object>> groupExpression)
{
this.groupDescription =
ObjectHelper.GetPropertyName(groupExpression);
return this;
}
public ICollectionView Run()
{
ICollectionView collectionView =
CollectionViewSource.GetDefaultView(GetItems(false));
collectionView = ApplySortOrGroup(
collectionView, SortOrGroup.Sort, sortDescription);
collectionView = ApplySortOrGroup(
collectionView, SortOrGroup.Group, groupDescription);
return collectionView;
}
public void RunThreadedWithCallback(
Action<ICollectionView> threadCallBack)
{
ICollectionView collectionView = null;
if (threadCallBack == null)
throw new ApplicationException("threadCallBack can not be null");
GetAll(
(data) =>
{
if (data != null)
{
collectionView = CollectionViewSource.GetDefaultView(data);
collectionView = ApplySortOrGroup(
collectionView, SortOrGroup.Sort, sortDescription);
collectionView = ApplySortOrGroup(
collectionView, SortOrGroup.Group, groupDescription);
}
threadCallBack(collectionView);
},
(ex) =>
{
throw ex;
});
}
#endregion
}
}
看看下面的方法有多简单?我们所做的就是设置一个内部字段来表示调用流畅 API 方法的操作,并返回我们自己(this)。
IsThreaded()
SortBy()
GroupBy()
流畅 API 的最后一部分是 Run()
或 RunThreadedWithCallback()
方法;这些方法预计是最后调用的。现在,没有任何东西可以阻止用户以他们想要的任何顺序调用这些方法,所以他们可以完全绕过
IsThreaded()
SortBy()
GroupBy()
方法调用,只调用 Run()
或 RunThreadedWithCallback()
,它返回一个 ICollectionView
,我们对此无能为力。但是,我们可以使用另一个 .NET 技巧,即扩展方法,所以即使他们绕过了正常的流畅 API 顺序,我们也可以使其看起来像一个流畅 API。
演示应用程序中有一个这方面的例子,我故意不遵循演示应用程序的流畅 API 并过早地调用 Run()
方法,该方法返回一个未排序/未分组的 ICollectionView
。这是那段代码:
DemoData = new DummyModelDataProvider()
//this actually returns ICollectionView,
//so we are effectively at end of Fluent API calls
.Run()
//But help is at hand, with some clever
//extension methods on ICollectionView, we can preempt
//the user doing this, and still get things to work,
//and make it look like a fluent API
.SortBy(x => x.LName, ListSortDirection.Ascending)
.GroupBy(x => x.Location);
但演示应用程序还提供了 ICollectionView
的一些扩展方法,这些方法可以某种程度上预防这种情况的发生,正如我们在上面的代码片段中所见,流畅性仍然得以保留。
以下是相关的 ICollectionView
扩展方法。这是一种廉价的花招,但在此案例中有效,并且是你可以在自己的流畅 API 中使用的东西。
public static class ProviderExtensions
{
public static ICollectionView SortBy(this ICollectionView icv,
Expression<Func<DummyModel, Object>> sortExpression,
ListSortDirection sortDirection)
{
icv.SortDescriptions.Add(new SortDescription(
ObjectHelper.GetPropertyName(sortExpression), sortDirection));
return icv;
}
public static ICollectionView GroupBy(this ICollectionView icv,
Expression<Func<DummyModel, Object>> groupExpression)
{
icv.GroupDescriptions.Add(
new PropertyGroupDescription(
ObjectHelper.GetPropertyName(groupExpression)));
return icv;
}
}
就这些
好了,就到这里。我知道这不像我平常的文章风格,但我也很喜欢写这类文章,所以如果你想投票/评论,请随意,我将不胜感激。