在C#中設計Fluent API
我們經常使用的一些框架例如:EF,Automaper,NHibernate等都提供了非常優秀的Fluent API, 這樣的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_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()方法抽象到一個類中,而將馬戲團的表演抽象到一個接口中:
public abstract class Performer
{
public abstract IList<string> PlayedItem { get; protected set; }
public abstract ICircusPlayer StartShow();
public abstract void EndShow();
}
public interface ICircusPlayer
{
ICircusPlayer MonkeysPlay();
ICircusPlayer ElephantsPlay();
ICircusPlayer TogetherPlay();
}
有了這樣的分類,我們重新設計API:
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。相比而言泛型方法中的類型時可以省略的,編譯器可以根據參數推斷出參數類型,例如
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);
}