C#中设计Fluent API
我们经常使用的一些框架例如:EF,Automaper,NHibernate等都提供了非常优秀的Fluent API, 这样的API充分利用了VS的智能提示,而且写出来的代码非常整洁。我们如何在代码中也写出这种Fluent的代码呢,我这里介绍3总比较常用的模式,在这些模式上稍加改动或者修饰就可以变成实际项目中可以使用的API,当然如果没有设计API的需求,对我们理解其他框架的代码也是非常有帮助。
一、最简单且最实用的设计
这是最常见且最简单的设计,每个方法内部都返回return this; 这样整个类的所有方法都可以一连串的写完。代码也非常简单:
使用起来也非常简单:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
public class CircusPerformer { public List<string> PlayedItem { get; private set; } public CircusPerformer() { PlayedItem=new List<string>(); } public CircusPerformer StartShow() { //make a speech and start to show return this; } public CircusPerformer MonkeysPlay() { //monkeys do some show PlayedItem.Add("MonkeyPlay"); return this; } public CircusPerformer ElephantsPlay() { //elephants do some show PlayedItem.Add("ElephantPlay"); return this; } public CircusPerformer TogetherPlay() { //all of the animals do some show PlayedItem.Add("TogetherPlay"); return this; } public void EndShow() { //finish the show } |
调用:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
[Test] public void All_shows_can_invoke_by_fluent_way() { //Arrange var circusPerformer = new CircusPerformer(); //Act circusPerformer .MonkeysPlay() .ElephantsPlay() .StartShow() .TogetherPlay() .EndShow(); //Assert circusPerformer.PlayedItem.Count.Should().Be(3); circusPerformer.PlayedItem.Contains("MonkeysPlay"); circusPerformer.PlayedItem.Contains("ElephantsPlay"); circusPerformer.PlayedItem.Contains("TogetherPlay"); } |
但是这样的API有个瑕疵,马戏团circusPerformer在表演时是有顺序的,首先要调用StartShow(),其次再进行各种表演,表演结束后要调用EndShow()结束表演,但是显然这样的API没法满足这样的需求,使用者可以随心所欲改变调用顺序。
我们知道,作为一个优秀的API,要尽量避免让使用者犯错,比如要设计private 字段,readonly 字段等都是防止使用者去修改内部数据从而导致出现意外的结果。
二、设计具有调用顺序的Fluent API
在之前的例子中,API设计者期望使用者首先调用StartShow()方法来初始化一些数据,然后进行表演,最后使用者方可调用EndShow(),实现的思路是将不同种类的功能抽象到不同的接口中或者抽象类中,方法内部不再使用return this,取而代之的是return INext;
根据这个思路,我们将StartShow(),和EndShow()方法抽象到一个类中,而将马戏团的表演抽象到一个接口中:
|
1
2
3
4
5
6
|
public abstract class Performer { public abstract IList<string> PlayedItem { get; protected set; } public abstract ICircusPlayer StartShow(); public abstract void EndShow(); } |
|
1
2
3
4
5
6
|
public interface ICircusPlayer { ICircusPlayer MonkeysPlay(); ICircusPlayer ElephantsPlay(); ICircusPlayer TogetherPlay(); } |
有了这样的分类,我们重新设计API:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
public class CircusPerfomer:Performer,ICircusPlayer { public override sealed IList<string> PlayedItem { get;protected set; } public CircusPerfomer() { PlayedItem = new List<string>(); } public override ICircusPlayer StartShow() { //make a speech and start to show return this; } public ICircusPlayer MonkeysPlay() { //monkeys do some show PlayedItem.Add("MonkeyPlay"); return this; } public ICircusPlayer ElephantsPlay() { //elephants do some show PlayedItem.Add("ElephantPlay"); return this; } public ICircusPlayer TogetherPlay() { //all of the animals do some show PlayedItem.Add("TogetherPlay"); return this; } public override void EndShow() { //finish the show } } |
这样的API可以满足我们的要求,在马戏团circusPerformer实例上只能调用StartShow()和EndShow(),调用完StartShow()后方可调用各种表演方法。
当然由于我们的API很简单,所以这个设计还算说得过去,如果业务很复杂,需要考虑众多的情形或者顺序我们可以进一步完善,实现的基本思想是利用装饰者模式和扩展方法,由于园子里的dax.net在很早前就发表了相关博客在C#中使用装饰器模式和扩展方法实现Fluent Interface,所以大家可以去看这篇文章的实现方案,该设计应该可以说是终极模式,实现过程也较为复杂。
三、泛型类的Fluent设计
泛型类中有个不算问题的问题,那就是泛型参数是无法省略的,当你在使用var list=new List<string>()这样的类型时,必须指定准确的类型string。相比而言泛型方法中的类型时可以省略的,编译器可以根据参数推断出参数类型,例如
|
1
2
3
|
var circusPerfomer = new CircusPerfomerWithGenericMethod(); circusPerfomer.Show<Dog>(new Dog()); circusPerfomer.Show(new Dog()); |
如果想省略泛型类中的类型有木有办法?答案是有,一种还算优雅的方式是引入一个非泛型的静态类,静态类中实现一个静态的泛型方法,方法最终返回一个泛型类型。这句话很绕口,我们不妨来看个一个画图板实例吧。
定义一个Drawing<TShape>类,此类可以绘出TShape类型的图案
|
1
2
3
4
5
6
7
8
9
10
|
public class Drawing<TShape> where TShape :IShape { public TShape Shape { get; private set; } public TShape Draw(TShape shape) { //drawing this shape Shape = shape; return shape; } } |
定义一个Canvas类,此类可以画出Pig,根据传入的基本形状,调用对应的Drawing<TShape>来组合出一个Pig来
|
1
2
3
4
5
6
7
8
9
|
public void DrawPig(Circle head, Rectangle mouth) { _history.Clear(); //use generic class, complier can not infer the correct type according to parameters Register( new Drawing<Circle>().Draw(head), new Drawing<Rectangle>().Draw(mouth) ); } |
这段代码本身是非常好懂的,而且这段代码也很clean。如果我们在这里想使用一下之前提到过的技巧,实现一个省略泛型类型且比较Fluent的方法我们可以这样设计:
首先这样的设计要借助于一个静态类:
|
1
2
3
4
5
6
7
|
public static class Drawer { public static Drawing<TShape> For<TShape>(TShape shape) where TShape:IShape { return new Drawing<TShape>(); } } |
然后利用这个静态类画一个Dog
|
1
2
3
4
5
6
7
8
9
|
public void DrawDog(Circle head, Rectangle mouth) { _history.Clear(); //fluent implements Register( Drawer.For(head).Draw(head), Drawer.For(mouth).Draw(mouth) ); } |
可以看到这里已经变成了一种Fluent的写法,写法同样比较clean。写到这里我脑海中浮现出来了一句”费这劲干嘛”,这也是很多人看到这里要想说的,我只能说你完全可以把这当成是一种奇技淫巧,如果哪天遇到使用的框架有这种API,你能明白这是怎么回事就行。
四、案例
写到这里我其实还想举一个例子来说说这种技巧在有些情况下是很常用的,大家在写EF配置,Automaper配置的时候经常这样写:
|
1
2
3
4
5
6
7
8
|
xx.MapPath( Path.For(_student).Property(x => x.Name), Path.For(_student).Property(x => x.Email), Path.For(_customer).Property(x => x.Name), Path.For(_customer).Property(x => x.Email), Path.For(_manager).Property(x => x.Name), Path.For(_manager).Property(x => x.Email) ) |
这样的写法就是前面的技巧改变而来,我们现在设计一个Validator,假如说这个Validator需要批量对Model的字段进行验证,我们也需要定义一个配置文件,配置某某Model的某某字段应该怎么样,利用这个配置我们可以验证出哪些数据不符合这个配置。
配置文件类Path的关键代码:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class Path<TModel> { private TModel _model; public Path(TModel model) { _model = model; } public PropertyItem<TValue> Property<TValue>(Expression<Func<TModel, TValue>> propertyExpression) { var item = new PropertyItem<TValue>(propertyExpression.PropertyName(), propertyExpression.PropertyValue(_model),_model); return item; } } |
为了实现fluent,我们还需要定义一个静态非泛型类,
|
1
2
3
4
5
6
7
8
|
public static class Path { public static Path<TModel> For<TModel>(TModel model) { var path = new Path<TModel>(model); return path; } } |
定义Validator,这个类可以读取到配置的信息,
|
1
2
3
4
5
6
7
8
|
public Validator<TValue> MapPath(params PropertyItem<TValue>[] properties) { foreach (var propertyItem in properties) { _items.Add(propertyItem); } return this; } |
最后调用
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
[Test] public void Should_validate_model_values() { //Arrange var validator = new Validator<string>(); validator.MapPath( Path.For(_student).Property(x => x.Name), Path.For(_student).Property(x => x.Email), Path.For(_customer).Property(x => x.Name), Path.For(_customer).Property(x => x.Email), Path.For(_manager).Property(x => x.Name), Path.For(_manager).Property(x => x.Email) ) .OnCondition((model)=>!string.IsNullOrEmpty(model.ToString())); //Act validator.Validate(); //Assert var result = validator.Result(); result.Count.Should().Be(3); result.Any(x => x.ModelType == typeof(Student) && x.Name == "Email").Should().Be(true); result.Any(x => x.ModelType == typeof(Customer) && x.Name == "Name").Should().Be(true); result.Any(x => x.ModelType == typeof(Manager) && x.Name == "Email").Should().Be(true); } |
结束语:有了这些Fluent API设计方式,大家在设计自己的API时可以设计出更优雅更符合语义的API,本文提供下载本文章所使用的源码,vs2013创建,测试项目使用了Nunit和FluentAssertions,如需转载请注明出处。
C#中设计Fluent API的更多相关文章
- 1.【使用EF Code-First方式和Fluent API来探讨EF中的关系】
原文链接:http://www.c-sharpcorner.com/UploadFile/3d39b4/relationship-in-entity-framework-using-code-firs ...
- EF里的默认映射以及如何使用Data Annotations和Fluent API配置数据库的映射
I.EF里的默认映射 上篇文章演示的通过定义实体类就可以自动生成数据库,并且EF自动设置了数据库的主键.外键以及表名和字段的类型等,这就是EF里的默认映射.具体分为: 数据库映射:Code First ...
- EF:Fluent API 把一对多映射为一对一
假设有两张表:A表和B表.A表与B表在数据库中的关系是一对多,但我们需要在EF中映射为一对一. 首先在A实体类和B实体类中互相为对方增加一个实体类的属性: public A { public B B ...
- 8.2 使用Fluent API进行实体映射【Code-First系列】
现在,我们来学习怎么使用Fluent API来配置实体. 一.配置默认的数据表Schema Student实体 using System; using System.Collections.Gener ...
- 8.3 使用Fluent API进行属性映射【Code-First系列】
现在,我打算学习,怎么用Fluent API来配置领域类中的属性. using System; using System.Collections.Generic; using System.Linq; ...
- EF Fluent API上
什么是Fluent API? 官方答案:EF 中内嵌的约定将 POCO 类映射到表.但是,有时您无法或不想遵守这些约定,需要将实体映射到约定指示外的其他对象,所以Fluent API和注解都是一种方 ...
- Entity Framework Code First 中使用 Fluent API 笔记。
在做MVC+EF CodeFirst 的Demo时,碰到的问题, 在组册用户时,要让用户输入确认密码,但是数据库中又不需要保存这个字段,解决方案很多了,这里我列出通过EF Code First的解决方 ...
- ORM系列之二:EF(4) 约定、注释、Fluent API
目录 1.前言 2.约定 2.1 主键约定 2.2 关系约定 2.3 复杂类型约定 3.数据注释 3.1 主键 3.2 必需 3.3 MaxLength和MinLength 3.4 NotMapped ...
- Fluent API 配置
EF里实体关系配置的方法,有两种: Data Annotation方式配置 也可以 Fluent API 方式配置 Fluent API 配置的方法 EF里的实体关系 Fluent API 配置分为H ...
随机推荐
- 同步(Synchronization)
多线程应用程序的存在,在运行打开一个潜在的多线程安全的接入资源. 两个线程相同的资源可能会以意想不到的方式改变相互干扰. 例如.一个线程可以覆盖有一个线程改变或使应用程序进入一个潜在的无效的状态未知. ...
- 实现DataGridView行的拖动,即实现行的顺序交换
参考:http://blog.csdn.net/soarheaven/article/details/3267379 1.界面准备 (1)首先在form中添加一个DataGridView控件,将默认A ...
- RH033读书笔记(12)-Lab 13 Finding and Processing Files
Sequence 1: Using find Scenario: Log in as user student. Devise and execute a find command that prod ...
- POJ3581:Sequence(后缀数组)
Description Given a sequence, {A1, A2, ..., An} which is guaranteed A1 > A2, ..., An, you are to ...
- [Python 学习] 两、在Linux使用平台Python
在本节,它介绍了Linux如何使用平台Python 1. Python安装. 今天,大多数把自己的版本号Python的,它不能被安装.假设你要安装它,可以使用相应的安装指令. Fedora:先以roo ...
- BZOJ 2588 Count on a tree (COT) 是持久的段树
标题效果:两棵树之间的首次查询k大点的权利. 思维:树木覆盖树,事实上,它是正常的树木覆盖了持久段树. 由于使用权值段树可以寻求区间k大,然后应用到持久段树思想,间隔可以做减法.详见代码. CODE: ...
- SQL Server 索引列的顺序——真的没关系吗
原文:SQL Server 索引列的顺序--真的没关系吗 翻译自:http://www.mssqltips.com/sqlservertip/2718/sql-server-index-column- ...
- QlikView一年计算,以最新的销售数据
总销量的新财年后年初今天是非常需要的学生经常会遇到,有两种思路: 1. 能Load当数据是生成一个称为场YTDFlag.这是本财年的时刻,本场会1,除此以外,0.因此,在报告中可使用非常方便Sum(S ...
- 一张图让你看清Java集合类(Java集合类的总结)
如今关于Java集合类的文章非常多,可是我近期看到一个非常有意思图片,基本上把Java集合的整体框架都给展现出来了.非常直观. watermark/2/text/aHR0cDovL2Jsb2cuY3N ...
- c#左右socket连接超时控制方案
之前有一个项目中使用Remoting技术.当远程地址无效或server不执行,访问远程对象的方法,它会经过几十秒的时间来抛出异常秒. 由于我使用tcp状态.因此,认为可以使用socket为了测试连接, ...