C#中设计Fluent API

C#中设计Fluent API

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

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

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

使用起来也非常简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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
      }

调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[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()方法抽象到一个类中,而将马戏团的表演抽象到一个接口中:

1
2
3
4
5
6
public  abstract  class  Performer
    {
        public  abstract  IList< string > PlayedItem { get ; protected  set ; }
        public  abstract  ICircusPlayer StartShow();
        public  abstract  void  EndShow();
    }
1
2
3
4
5
6
public  interface  ICircusPlayer
    {
        ICircusPlayer MonkeysPlay();
        ICircusPlayer ElephantsPlay();
        ICircusPlayer TogetherPlay();
    }

有了这样的分类,我们重新设计API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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。相比而言泛型方法中的类型时可以省略的,编译器可以根据参数推断出参数类型,例如

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

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

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

1
2
3
4
5
6
7
8
9
10
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来

1
2
3
4
5
6
7
8
9
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的方法我们可以这样设计:

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

1
2
3
4
5
6
7
public  static  class  Drawer
  {
      public  static  Drawing<TShape> For<TShape>(TShape shape) where  TShape:IShape
      {
          return  new  Drawing<TShape>();
      }
  }

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

1
2
3
4
5
6
7
8
9
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配置的时候经常这样写:

1
2
3
4
5
6
7
8
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的关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
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,我们还需要定义一个静态非泛型类,

1
2
3
4
5
6
7
8
public  static  class  Path
     {
         public  static  Path<TModel> For<TModel>(TModel model)
         {
             var  path = new  Path<TModel>(model);
             return  path;
         }
     }

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

1
2
3
4
5
6
7
8
public  Validator<TValue> MapPath( params  PropertyItem<TValue>[] properties)
       {
           foreach  ( var  propertyItem in  properties)
           {
               _items.Add(propertyItem);
           }
           return  this ;
       }

最后调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[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设计方式,大家在设计自己的API时可以设计出更优雅更符合语义的API,本文提供下载本文章所使用的源码,vs2013创建,测试项目使用了Nunit和FluentAssertions,如需转载请注明出处。

你可能感兴趣的:(api)