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,如需转载请注明出处。

C#中设计Fluent API的更多相关文章

  1. 1.【使用EF Code-First方式和Fluent API来探讨EF中的关系】

    原文链接:http://www.c-sharpcorner.com/UploadFile/3d39b4/relationship-in-entity-framework-using-code-firs ...

  2. EF里的默认映射以及如何使用Data Annotations和Fluent API配置数据库的映射

    I.EF里的默认映射 上篇文章演示的通过定义实体类就可以自动生成数据库,并且EF自动设置了数据库的主键.外键以及表名和字段的类型等,这就是EF里的默认映射.具体分为: 数据库映射:Code First ...

  3. EF:Fluent API 把一对多映射为一对一

    假设有两张表:A表和B表.A表与B表在数据库中的关系是一对多,但我们需要在EF中映射为一对一. 首先在A实体类和B实体类中互相为对方增加一个实体类的属性: public A { public B B ...

  4. 8.2 使用Fluent API进行实体映射【Code-First系列】

    现在,我们来学习怎么使用Fluent API来配置实体. 一.配置默认的数据表Schema Student实体 using System; using System.Collections.Gener ...

  5. 8.3 使用Fluent API进行属性映射【Code-First系列】

    现在,我打算学习,怎么用Fluent API来配置领域类中的属性. using System; using System.Collections.Generic; using System.Linq; ...

  6. EF Fluent API上

     什么是Fluent API? 官方答案:EF 中内嵌的约定将 POCO 类映射到表.但是,有时您无法或不想遵守这些约定,需要将实体映射到约定指示外的其他对象,所以Fluent API和注解都是一种方 ...

  7. Entity Framework Code First 中使用 Fluent API 笔记。

    在做MVC+EF CodeFirst 的Demo时,碰到的问题, 在组册用户时,要让用户输入确认密码,但是数据库中又不需要保存这个字段,解决方案很多了,这里我列出通过EF Code First的解决方 ...

  8. ORM系列之二:EF(4) 约定、注释、Fluent API

    目录 1.前言 2.约定 2.1 主键约定 2.2 关系约定 2.3 复杂类型约定 3.数据注释 3.1 主键 3.2 必需 3.3 MaxLength和MinLength 3.4 NotMapped ...

  9. Fluent API 配置

    EF里实体关系配置的方法,有两种: Data Annotation方式配置 也可以 Fluent API 方式配置 Fluent API 配置的方法 EF里的实体关系 Fluent API 配置分为H ...

随机推荐

  1. Linux/UNIX数据文件和信息系统

    数据文件和信息系统 密码文件 在存储/etc/passwd在.以下功能可以用来获得密码文件条目. #include <sys/types.h> #include <pwd.h> ...

  2. JS call与apply

    JS的call与apply call和apply是JS中比较重要的两个方法, 一般在框架和组件设计中用的较多,比如jQuery Code. 那么这两个方法是做什么的呢,下面我们通过代码来了解: 1 f ...

  3. 【Android进阶】判断网络连接状态并自动界面跳转

    用于判断软件打开时的网络连接状态,若无网络连接,提醒用户跳转到设置界面 /** * 设置在onStart()方法里面,可以在界面每次获得焦点的时候都进行检测 */ @Override protecte ...

  4. iOS经常使用类别

    我们发现,慢慢积累了很多自己写的各种类别的. .今天,无私.张贴 1.NSDateFomatter @interface NSDateFormatter (MyCategory) + (id)date ...

  5. iOS在地图上WGS84、GCJ-02、BD-09互转解决方案

    该项目的最新进展包括地图共享模块,android同事集团开始,使用百度地图sdk,我开始回,运用iOS SDK的mapkit做,之后,问题是,用纬度和经度坐标iOS端和Android端出现了比較大偏差 ...

  6. js在方法Ajax请求数据来推断,验证无效(OnClientClick=&quot;return Method();&quot;),或者直接运行的代码隐藏

    function CheckAdd() { var flag = true; $.ajax({ cache: false, async: false, url: "/ajaxpage/get ...

  7. QTbutton设置背景颜色和文字显示位置设置

    QPushButton * pQBtn = new QPushButton( cBuff, this ); pQBtn->setStyleSheet("text-align: left ...

  8. MVC 分离Controllers-Views

    将MVC中的Controllers.Model和View分别放到单独的项目中 Model: 新建-项目-Windows-类库 MVCTest.Model Controller:新建-项目-Window ...

  9. height/innerHeight/outerHeight

    <script> $(document).ready(function(){ alert("height:"+$("#div").height()) ...

  10. 王立平--result += &quot;{&quot;;

    result += "{"; 等于:result=result+"{" 字符串连接 x+=1====x=x+1 版权声明:本文博客原创文章,博客,未经同意,不得 ...