我们经常使用的一些框架例如:EF,Automaper,NHibernate等都提供了非常优秀的Fluent Interface, 这样的API充分利用了VS的智能提示,而且写出来的代码非常整洁。我们如何在代码中也写出这种Fluent的代码呢,我这里介绍3总比较常用的模式,在这些模式上稍加改动或者修饰就可以变成实际项目中可以使用的API,当然如果没有设计API的需求,对我们理解其他框架的代码也是非常有帮助。

一、最简单且最实用的设计

这是最常见且最简单的设计,每个方法内部都返回return this; 这样整个类的所有方法都可以一连串的写完。代码也非常简单:

使用起来也非常简单:

       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
}

调用:

        [Test]
public void All_shows_can_be_invoked_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没法满足这样的需求,使用者可以随心所欲改变调用顺序。

如上图所示,vs将所有的方法都提示了出来。

我们知道,作为一个优秀的API,要尽量避免让使用者犯错,比如要设计private 字段,readonly 字段等都是防止使用者去修改内部数据从而导致出现意外的结果。

二、设计具有调用顺序的Fluent API

在之前的例子中,API设计者期望使用者首先调用StartShow()方法来初始化一些数据,然后进行表演,最后使用者方可调用EndShow(),实现的思路是将不同种类的功能抽象到不同的接口中或者抽象类中,方法内部不再使用return this,取而代之的是return INext;

根据这个思路,我们将StartShow(),和EndShow()方法抽象到一个类中,而将马戏团的表演抽象到一个接口中:

      public abstract class Performer
{
public abstract ICircusPlayer CircusPlayer { get; }
public abstract ICircusPlayer StartShow();
public abstract void EndShow();
}
       public interface ICircusPlayer
{
IList PlayedItem { get; }
ICircusPlayer MonkeysPlay();
ICircusPlayer ElephantsPlay();
Performer TogetherPlay();
}

有了这样的分类,我们重新设计API,将StartShow()和EndShow()设计在CircusPerfomer中,将马戏团的表演项目设计在CircusPlayer中:

      public class CircusPerformer:Performer
{
private ICircusPlayer _circusPlayer; override public ICircusPlayer CircusPlayer { get { return _circusPlayer; } } public override ICircusPlayer StartShow()
{
//make a speech and start to show
_circusPlayer=new CircusPlayer(this);
return _circusPlayer;
} public override void EndShow()
{
//finish the show
}
}
      public class CircusPlayer:ICircusPlayer
{
private readonly Performer _performer;
public IList PlayedItem { get; private set; } public CircusPlayer(Performer performer)
{
_performer = performer;
PlayedItem=new List();
} public ICircusPlayer MonkeysPlay()
{
PlayedItem.Add("MonkeyPlay");
//monkeys do some show
return this;
} public ICircusPlayer ElephantsPlay()
{
PlayedItem.Add("ElephantPlay");
//elephants do some show
return this;
} public Performer TogetherPlay()
{
PlayedItem.Add("TogetherPlay");
//all of the animals do some show
return _performer;
}
}

这样的API可以满足我们的要求,在马戏团circusPerformer实例上只能调用StartShow()和EndShow()

调用完StartShow()后方可调用各种表演方法。

当然由于我们的API很简单,所以这个设计还算说得过去,如果业务很复杂,需要考虑众多的情形或者顺序我们可以进一步完善,实现的基本思想是利用装饰者模式和扩展方法,由于园子里的dax.net在很早前就发表了相关博客在C#中使用装饰器模式和扩展方法实现Fluent Interface,所以大家可以去看这篇文章的实现方案,该设计应该可以说是终极模式,实现过程也较为复杂。

三、泛型类的Fluent设计

泛型类中有个不算问题的问题,那就是泛型参数是无法省略的,当你在使用var list=new List<string>()这样的类型时,必须指定准确的类型string。相比而言泛型方法中的类型时可以省略的,编译器可以根据参数推断出参数类型,例如

             var circusPerfomer = new CircusPerfomerWithGenericMethod();
circusPerfomer.Show<Dog>(new Dog());
circusPerfomer.Show(new Dog());

如果想省略泛型类中的类型有木有办法?答案是有,一种还算优雅的方式是引入一个非泛型的静态类,静态类中实现一个静态的泛型方法,方法最终返回一个泛型类型。这句话很绕口,我们不妨来看个一个画图板实例吧。

定义一个Drawing<TShape>类,此类可以绘出TShape类型的图案

    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来

          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的方法我们可以这样设计:

首先这样的设计要借助于一个静态类:

       public static class Drawer
{
public static Drawing<TShape> For<TShape>(TShape shape) where TShape:IShape
{
return new Drawing<TShape>();
}
}

然后利用这个静态类画一个Dog

          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配置的时候经常这样写:

     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的关键代码:

      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,我们还需要定义一个静态非泛型类,

    public static class Path
{
public static Path<TModel> For<TModel>(TModel model)
{
var path = new Path<TModel>(model);
return path;
}
}

定义Validator,这个类可以读取到配置的信息,

          public Validator<TValue> MapPath(params PropertyItem<TValue>[] properties)
{
foreach (var propertyItem in properties)
{
_items.Add(propertyItem);
}
return this;
}

最后调用

          [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语言更加清晰并且不失优雅, Path.For(A).Property(x=>x.Name).OnCondition(B),这句话可以翻译为,对A的属性Name设置条件为B。

结束语:有了这些Fluent API设计方式,大家在设计自己的API时可以设计出更优雅更符合语义的API,本文提供下载本文章所使用的源码,vs2013创建,测试项目使用了Nunit和FluentAssertions,如需转载请注明出处。

使用C#设计Fluent Interface的更多相关文章

  1. 连贯接口(fluent interface)的Java实现及应用。

    几年前在单元测试时使用mockito和junit(使用hamcrest提供的比较方法)的时候,就用到过这样类似的语法: mockito: when(mock.someMethod("some ...

  2. Java链式方法 连贯接口(fluent interface)

    有两种情况可运用链式方法: 第一种  除最后一个方法外,每个方法都返回一个对象 object2 = object1.method1(); object3 = object2.method2(); ob ...

  3. Fluent interface

    In software engineering, a fluent interface (as first coined by Eric Evans and Martin Fowler) is an ...

  4. Fluent Interface(流式接口)

    我最初接触这个概念是读自<<模式-工程化实现及扩展>>,另外有Martin fowler大师 所写http://martinfowler.com/bliki/FluentInt ...

  5. [JavaScript,Java,C#,C++,Ruby,Perl,PHP,Python][转]流式接口(Fluent interface)

    原文:https://en.m.wikipedia.org/wiki/Fluent_interface(英文,完整) 转载:https://zh.wikipedia.org/wiki/流式接口(中文, ...

  6. 基于JDK动态代理实现的接口链式调用(Fluent Interface)工具

    什么是链式接口(Fluent Interface) 根据wikipedia上的定义,Fluent interface是一种通过链式调用方法来完成方法的调用,其操作分为终结与中间操作两种.[1] 下面是 ...

  7. 微软最新设计Fluent Design System初体验

    微软最新设计Fluent Design System初体验 本文图片不全!建议移步知乎专栏查看!!! https://zhuanlan.zhihu.com/p/30582886 原创 2017-11- ...

  8. 流畅设计 Fluent Design System 中的光照效果 RevealBrush,WPF 也能模拟实现啦!

    UWP 才能使用的流畅设计效果好惊艳,写新的 UWP 程序可以做出更漂亮的 UI 啦!然而古老的 WPF 项目也想解解馋怎么办? 于是我动手实现了一个!   迫不及待看效果 ▲ 是不是很像 UWP 中 ...

  9. 安卓手机的屏幕规格很多。app开发者在设计User Interface的时候,要怎么处理,才能适应不同屏幕大小?

    在app store下载应用时经常看到:此App已针对iPhone 5 进行优化.可是Android手机屏幕规格这么多,相差这么远.难道要针对每个尺寸都进行一次优化吗?(题主非专业人士,看到2014年 ...

随机推荐

  1. 关于myeclipse的破解的问题

    myeclipse的破解的问题,也是在网上down 了一下,发现并不需要找到什么注册的软件都可以自动完成的哦! 博客地址:http://blog.csdn.net/fuxiaohui/article/ ...

  2. Balance - 七夕悠然

    想争取一个月至少一篇博客的,还是没搭上七月的末班车.两个小妹妹来上海看我了,工作上又有点儿忙,充分利用所有时间了,还是没有挪出时间来写东西,貌似写东西也要时机一样,需要在可以静静思考的时候,再加上有淡 ...

  3. Codeigniter 在Active Record中限制批量更新数目

    今天手头电商项目有个需求是:将订单中的优惠券自动发放给买家,所以要只更新优惠券表中的某几行数据,查了手册和网络都没有解决办法. 一开始用循环和遍历来做都是错的,因为update语句一下就更新掉所有符合 ...

  4. Object 转化为String时的一个问题 null->"null"

    近日在工作出了一个较大的问题,导致被客户投诉. 事情大致是,某个功能里新增对用户手机的修改,在平台数据同步过程中,出现了将用户以前的要同步的数据,那时还没有手机号码所以是null,新功能上线后,将手机 ...

  5. 【转】测试LibreOffice SDK 开发环境配置(Windows)

    原文:http://www.aqcoder.com/blog/detail/id/7441186b-93fd-482c-b4d7-0facd1ee498d 下载与安装 LibreOffice 主页:h ...

  6. Android广播大全

    1.String ADD_SHORTCUT_ACTION 动作:在系统中添加一个快捷方式. 2.String ALL_APPS_ACTION 动作:列举所有可用的应用.输入:无. 3.String A ...

  7. 62. Unique Paths && 63 Unique Paths II

    https://leetcode.com/problems/unique-paths/ 这道题,不利用动态规划基本上规模变大会运行超时,下面自己写得这段代码,直接暴力破解,只能应付小规模的情形,当23 ...

  8. git svn clone时间估算

    处理器: Intel(R) Xeon(R) CPU E5-2620 @2.00GHz 2.00Ghz 内存:32.0 GB 操作系统: Windows Server 2008 R2 Enterpris ...

  9. 【Android UI】:Fragment官方文档

    概述   Fragment表现Activity中UI的一个行为或者一部分.可以将多个fragment组合在一起,放在一个单独的activity中来创建一个多界面区域的UI,并可以在多个activity ...

  10. [LeetCode] 423 Reconstruct Original Digits from English

    Given a non-empty string containing an out-of-order English representation of digits 0-9, output the ...