使用C#设计Fluent Interface
我们经常使用的一些框架例如: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。