天天看點

使用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沒法滿足這樣的需求,使用者可以随心所欲改變調用順序。

使用C#設計Fluent Interface

如上圖所示,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()

使用C#設計Fluent Interface

調用完StartShow()後方可調用各種表演方法。

使用C#設計Fluent Interface

當然由于我們的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,如需轉載請注明出處。

作者:Richie Zhang

來源:http://www.cnblogs.com/richieyang/

聲明:本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。