本文翻译自 How to Design and Implement the Fluent Interface Pattern in C# | ASSIST Software Romania
什么是流式接口 (Fluent interface)?
流式接口(Fluent interface)是软件工程中一种面向对象 API 的实现方式,其面向广泛依赖于方法链设计,目的在于通过创建领域特定语言 (Domain-specific language, DSL) 来提高代码可读性。
前言
本文包含了多种变成概念,包括但不限于:继承、多态、方法链、领域特定语言、流式接口等。你可能不熟悉流式接口 API 这个术语,但你非常有可能在以前就已经使用过,因为它在许多面向对象的编程语言的很多框架与库中都有被一直使用过。
流式接口与扩展方法
流式接口与扩展方法都不是很简单,或者说是有时候甚至不太可能调试的模式,因为他们的中间结果在调试中都不可达。两者的语法很相似,但还是让我们看看它们之间的区别在哪里:
扩展方法
- 返回类型是不变的,其总会返回一个具有相同类型的对象
- 循环无法被阻止,开发者可以连接调用相同的方法,每次调用时方法都会被执行
- 语句会被一行一行执行,其结果取决于之前的操作
流式接口
- 每个方法的返回类型可以不同,这保证了灵活性与可扩展性
- 循环可以被限制,方法可以按照预先决定好的顺序连接
- 语句会在最后被执行,方法链用来收集数据与建立路径
主题
假如要实现一个类包含以下功能:
* 下载到文件/流与从文件/流上传
* 本机计算机与 Azure Blob 存储之间的文件传输
* 异步传输,这可能不着急,但稍后可能会需要
按标准方式,实现一下接口将满足需求:
interface IBlobTransfer
{
void BlobDownload(string connectionString, string blobName, string fileName, string filePath);
void BlobDownload(string connectionString, string blobName, string fileName, Stream stream);
void BlobUpload(string connectionString, string blobName, string fileName, string filePath);
void BlobUpload(string connectionString, string blobName, string fileName, Stream stream);
}
尽管这些代码可以满足需求,但让我们以一种更好的方式来实现他们。
设计
定义自然语言句法
首先,有必要定义一个包含所有合适且允许的方法组合列表,将此组合列表应用于上述主题将会生成以下结果:
FluentBlobTransfer.Connect(..).OnBlob(..).Download(..).ToFile(..);
FluentBlobTransfer.Connect(..).OnBlob(..).Download(..).ToStream(..);
FluentBlobTransfer.Connect(..).OnBlob(..).Upload(..).FromFile(..);
FluentBlobTransfer.Connect(..).OnBlob(..).Upload(..).FromStream(..);
约束
可以发现Download.FromFile
与Upload.ToStream
是不合法的组合。为了阻止这样的行为,必须满足以下条件:
Download
方法应该返回一个带有以下方法签名的接口:
void ToFile(..);
void ToStream(..);
Upload
方法应该返回一个带有以下方法签名的接口:
void FromFile(..);
void FromStream(..)
类图
定义接口
这是方法签名所在的位置,遵循这些简单的规则来保证成功:
- 第一个和最后一个方法可以是无参的
- 每个方法最多接受一个参数
- 使用描述性词汇,如最多五个单词组成的短语
- 覆盖所有情况,正确分割路径
interface IAzureBlob
{
IAzureAction OnBlob(string blobBlockPath);
}
interface IAzureAction
{
IAzureWrite Download(string fileName);
IAzureRead Upload(string fileName);
}
interface IAzureWrite
{
void ToFile(string filePath);
void ToStream(Stream stream);
}
interface IAzureRead
{
void FromFile(string filePath);
void FromStream(Stream stream);
}
实现
要点
- 构造函数必须是私有的,不允许类的实例化
- 不允许继承,使用
sealed
密闭修饰符 - 为方法和变量使用释义正确的名称,不使用含糊歧义的名称
- 每个方法最多接受一个参数,别忘了我们要以流畅的方式实现!
入口函数
入口函数需标记为static
静态,建议名称使用动词来表达意图。
实现接口与保证合适的方法链
方法链中的每一步都应该用来收集数据后指向下一个方法。通常,方法链的最后一个方法返回void
未知类型指针,来表明方法链链尾。
请注意,FluentBlobTransfer
类需要实现所有先前准备好的接口。
public sealed class FluentBlobTransfer : IAzureBlob, IAzureAction, IAzureWrite, IAzureRead
{
private readonly string connectionString;
private string blobBlockPath;
private string fileName;
private FluentBlobTransfer(string connectionString) => this.connectionString = connectionString;
public static IAzureBlob Connect(string connectionString) => new FluentBlobTransfer(connectionString);
public IAzureAction OnBlob(string blobBlockPath)
{
this.blobBlockPath = blobBlockPath;
return this;
}
public IAzureWrite Download(string fileName)
{
this.fileName = fileName;
return this;
}
public IAzureRead Upload(string fileName)
{
this.fileName = fileName;
return this;
}
public void ToFile(string filePath)
{
// Code to download from Azure Blob Storage to file
}
public void ToStream(Stream stream)
{
// Code to download from Azure Blob Storage to stream
}
public void FromFile(string filePath)
{
// Code to upload from file to Azure Blob Storage
}
public void FromStream(Stream stream)
{
// Code to upload from stream to Azure Blob Storage
}
}
请注意,传输过程的具体实现不在本文的讨论范围内。
现在你可以流畅地调用方法传输文件:
FluentBlobTransfer
.Connect("storageAccountConnectionString")
.OnBlob("blobName")
.Download("fileName")
.ToFile(@"D:\Azure\Downloads\");
探索
我最喜欢的流式接口的例子是Microsoft.Azure.*.Fluent
库,这些库的基础指出在于它们通过不言自明的流式接口取代了过往需要记忆命令和参数的需要(不像 PowerShell 脚本)。
下一个代码片段在一个新的资源组中创建了一个新的 SQL Server 数据库,并包含了两个数据库及一些防火墙规则。
await Azure
.Configure()
.WithLogLevel(Level.Basic)
.Authenticate(credentials)
.WithSubscription("subscriptionId")
.SqlServers
.Define("sqlServerName")
.WithRegion(Region.EuropeWest)
.WithNewResourceGroup()
.WithAdministratorLogin("sqlAdmin")
.WithAdministratorPassword("pass")
.WithNewDatabase("databaseOne")
.WithNewDatabase("databaseTwo")
.WithNewFirewallRule("0.0.0.0")
.WithNewFirewallRule("1.1.1.1")
.WithNewFirewallRule("2.2.2.2")
.CreateAsync();
EntityFramework 的流式接口配置提供了一个使用优秀句法配置数据库表与字段的方式。
modelBuilder
.Entity<Foo>()
.HasMany(foo => foo.Bars)
.WithOne(bar => bar.Foo)
.HasForeignKey(bar => bar.FooId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder
.Entity<Foo>()
.Property(foo => foo.Value)
.HasDefaultValue("0123456789")
.HasMaxLength(10)
.IsFixedLength()
.IsRequired();
在本文原作者的 GitHub 账户中可以浏览其他流式接口样例、探索流式方法链、访问服务本文的源代码。
结论
设计与实现一个易于解释与使用的流式接口会花费很多时间,但这样的努力非常值得。你可能会觉得它让你无法抗拒,但也要记住不是所有的都有必要以流式实现。
为可重用代码提供流式接口很方便,开发者可以不用担心“它是怎么工作的?”“我需要多少参数?”“多有少个选项?”就可以直接调用。
Visits: 204