C#中設(shè)計、使用Fluent API
我們經(jīng)常使用的一些框架例如:EF,Automaper,NHibernate等都提供了非常優(yōu)秀的Fluent API, 這樣的API充分利用了VS的智能提示,而且寫出來的代碼非常整潔。我們?nèi)绾卧诖a中也寫出這種Fluent的代碼呢,我這里介紹3總比較常用的模式,在這些模式上稍加改動或者修飾就可以變成實(shí)際項(xiàng)目中可以使用的API,當(dāng)然如果沒有設(shè)計API的需求,對我們理解其他框架的代碼也是非常有幫助。
一、最簡單且最實(shí)用的設(shè)計
這是最常見且最簡單的設(shè)計,每個方法內(nèi)部都返回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 }
調(diào)用:
[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有個瑕疵,馬戲團(tuán)circusPerformer在表演時是有順序的,首先要調(diào)用StartShow(),其次再進(jìn)行各種表演,表演結(jié)束后要調(diào)用EndShow()結(jié)束表演,但是顯然這樣的API沒法滿足這樣的需求,使用者可以隨心所欲改變調(diào)用順序。
如上圖所示,vs將所有的方法都提示了出來。
我們知道,作為一個優(yōu)秀的API,要盡量避免讓使用者犯錯,比如要設(shè)計private 字段,readonly 字段等都是防止使用者去修改內(nèi)部數(shù)據(jù)從而導(dǎo)致出現(xiàn)意外的結(jié)果。
二、設(shè)計具有調(diào)用順序的Fluent API
在之前的例子中,API設(shè)計者期望使用者首先調(diào)用StartShow()方法來初始化一些數(shù)據(jù),然后進(jìn)行表演,最后使用者方可調(diào)用EndShow(),實(shí)現(xiàn)的思路是將不同種類的功能抽象到不同的接口中或者抽象類中,方法內(nèi)部不再使用return this,取而代之的是return INext;
根據(jù)這個思路,我們將StartShow(),和EndShow()方法抽象到一個類中,而將馬戲團(tuán)的表演抽象到一個接口中:
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(); }
有了這樣的分類,我們重新設(shè)計API,將StartShow()和EndShow()設(shè)計在CircusPerfomer中,將馬戲團(tuán)的表演項(xiàng)目設(shè)計在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可以滿足我們的要求,在馬戲團(tuán)circusPerformer實(shí)例上只能調(diào)用StartShow()和EndShow()
調(diào)用完StartShow()后方可調(diào)用各種表演方法。
當(dāng)然由于我們的API很簡單,所以這個設(shè)計還算說得過去,如果業(yè)務(wù)很復(fù)雜,需要考慮眾多的情形或者順序我們可以進(jìn)一步完善,實(shí)現(xiàn)的基本思想是利用裝飾者模式和擴(kuò)展方法,由于園子里的dax.net在很早前就發(fā)表了相關(guān)博客在C#中使用裝飾器模式和擴(kuò)展方法實(shí)現(xiàn)Fluent Interface,所以大家可以去看這篇文章的實(shí)現(xiàn)方案,該設(shè)計應(yīng)該可以說是終極模式,實(shí)現(xiàn)過程也較為復(fù)雜。
三、泛型類的Fluent設(shè)計
泛型類中有個不算問題的問題,那就是泛型參數(shù)是無法省略的,當(dāng)你在使用var list=new List<string>()這樣的類型時,必須指定準(zhǔn)確的類型string。相比而言泛型方法中的類型時可以省略的,編譯器可以根據(jù)參數(shù)推斷出參數(shù)類型,例如
var circusPerfomer = new CircusPerfomerWithGenericMethod(); circusPerfomer.Show<Dog>(new Dog()); circusPerfomer.Show(new Dog());
如果想省略泛型類中的類型有木有辦法?答案是有,一種還算優(yōu)雅的方式是引入一個非泛型的靜態(tài)類,靜態(tài)類中實(shí)現(xiàn)一個靜態(tài)的泛型方法,方法最終返回一個泛型類型。這句話很繞口,我們不妨來看個一個畫圖板實(shí)例吧。
定義一個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,根據(jù)傳入的基本形狀,調(diào)用對應(yīng)的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。如果我們在這里想使用一下之前提到過的技巧,實(shí)現(xiàn)一個省略泛型類型且比較Fluent的方法我們可以這樣設(shè)計:
首先這樣的設(shè)計要借助于一個靜態(tài)類:
public static class Drawer { public static Drawing<TShape> For<TShape>(TShape shape) where TShape:IShape { return new Drawing<TShape>(); } }
然后利用這個靜態(tài)類畫一個Dog
public void DrawDog(Circle head, Rectangle mouth) { _history.Clear(); //fluent implements Register( Drawer.For(head).Draw(head), Drawer.For(mouth).Draw(mouth) ); }
可以看到這里已經(jīng)變成了一種Fluent的寫法,寫法同樣比較clean。寫到這里我腦海中浮現(xiàn)出來了一句”費(fèi)這勁干嘛”,這也是很多人看到這里要想說的,我只能說你完全可以把這當(dāng)成是一種奇技淫巧,如果哪天遇到使用的框架有這種API,你能明白這是怎么回事就行。
四、案例
寫到這里我其實(shí)還想舉一個例子來說說這種技巧在有些情況下是很常用的,大家在寫EF配置,Automaper配置的時候經(jīng)常這樣寫:
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) )
這樣的寫法就是前面的技巧改變而來,我們現(xiàn)在設(shè)計一個Validator,假如說這個Validator需要批量對Model的字段進(jìn)行驗(yàn)證,我們也需要定義一個配置文件,配置某某Model的某某字段應(yīng)該怎么樣,利用這個配置我們可以驗(yàn)證出哪些數(shù)據(jù)不符合這個配置。
配置文件類Path的關(guān)鍵代碼:
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; } }
為了實(shí)現(xiàn)fluent,我們還需要定義一個靜態(tài)非泛型類,
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; }
最后調(diào)用
[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語言更加清晰并且不失優(yōu)雅, Path.For(A).Property(x=>x.Name).OnCondition(B),這句話可以翻譯為,對A的屬性Name設(shè)置條件為B。
相關(guān)文章
C#中sqlDataRead 的三種方式遍歷讀取各個字段數(shù)值的方法
這篇文章主要介紹了C#中 sqlDataRead 的三種方式遍歷讀取各個字段數(shù)值的方法,每種方法給大家介紹的都非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2018-09-09C#.Net基于正則表達(dá)式抓取百度百家文章列表的方法示例
這篇文章主要介紹了C#.Net基于正則表達(dá)式抓取百度百家文章列表的方法,結(jié)合實(shí)例形式分析了C#獲取百度百家文章內(nèi)容及使用正則表達(dá)式匹配標(biāo)題、內(nèi)容、地址等相關(guān)操作技巧,需要的朋友可以參考下2017-08-08unity實(shí)現(xiàn)延遲回調(diào)工具
這篇文章主要為大家詳細(xì)介紹了unity實(shí)現(xiàn)延遲回調(diào)工具,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-09-09C#中在WebClient中使用post發(fā)送數(shù)據(jù)實(shí)現(xiàn)方法
這篇文章主要介紹了C#中在WebClient中使用post發(fā)送數(shù)據(jù)實(shí)現(xiàn)方法,需要的朋友可以參考下2014-08-08C# 靜態(tài)構(gòu)造函數(shù)使用總結(jié)
今天花了一些時間把靜態(tài)構(gòu)造函數(shù)的用法總結(jié)了一下,希望高手們指點(diǎn)。謝謝2013-03-03