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. 【Android进阶】Activity的四种加载模式

    Activity的四种加载模式: 1.standard :系统的默认模式,一次跳转即会生成一个新的实例.假设有一个activity命名为Act1, 执行语句:startActivity(new Int ...

  2. RH033读书笔记(14)-Lab 15 Switching Users and Setting a Umask

    Lab 15 Switching Users and Setting a Umask Goal: Become familiar with the use of several essential c ...

  3. 《Pro Android Graphics》读第三季度票据

    Android Frame Animation: XML, Concepts and Optimization Frame Animation Concepts: Cels, Framerate, a ...

  4. webstorm创建nodejs + express + jade 的web 项目

    webstorm创建nodejs + express + jade 的web 项目 前简单了解过nodejs,觉得用nodejs来做个网站也太麻烦了,要自己拼html的字符串返回,这能做网站嘛? 最近 ...

  5. Sublime Text 3 搭建Go开发环境(Windows)

    一.安装GO 如果已经环境已经配置好,这一步省略.... 1.下载并安装go sdk 2.配置环境变量 (1). 新建 变量名:GOBIN 变量值 :F:\Go\bin (2). 新建 变量名:GOA ...

  6. 大数据的胖哥的方式(9)- 金融业数据仓库的逻辑模型FS-LDM

    介绍: 大数据是不是海市蜃楼,来自小橡子只是意淫奥克斯,大数据的发展,而且要从头开始,基于大数据建设国家.项目-level数据中心行业将越来越多,大数据仅供技术,而非溶液,临数据组织模式,数据逻辑模式 ...

  7. python基础课程_学习笔记26:编程的乐趣

    编程的乐趣 编程柔术 当你坐下来,打算如何组织计划要定时,具体程序,然而,无论什么经验.在实现时间的函数的,你会逐渐学会了原来的设计,实用的新知识.我们不应该忽视沿途汲取的教训,相反,它们用于其他设计 ...

  8. 第十七章——配置SQLServer(4)——优化SQLServer实例的配置

    原文:第十七章--配置SQLServer(4)--优化SQLServer实例的配置 前言: Sp_configure 可以用于管理和优化SQLServer资源,而且绝大部分配置都可以使用SQLServ ...

  9. 关闭 MsMpEng.exe

    MsMpEng.exe是Windows Defender 自动保护服务的核心引擎. 系统是win8.1 最近发现MsMpEng.exe是任务管理器里面最占内存的一个程序,且无法强制结束程序.偶然发现一 ...

  10. Ceph 存储集群

    Ceph 存储集群 Ceph 作为软件定义存储的代表之一,最近几年其发展势头很猛,也出现了不少公司在测试和生产系统中使用 Ceph 的案例,尽管与此同时许多人对它的抱怨也一直存在.本文试着整理作者了解 ...