C#中设计、使用Fluent API

我们经常使用的一些框架例如:EF,Automaper,NHibernate等都提供了非常优秀的Fluent API, 这样的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没法满足这样的需求,使用者可以随心所欲改变调用顺序。

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

调用完StartShow()后方可调用各种表演方法。

当然由于我们的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。

(0)

相关推荐

  • C#中实现Fluent Interface的三种方法

    背景知识 Fluent Interface是一种通过连续的方法调用以完成特定逻辑处理的API实现方式,在代码中引入Fluent Interface不仅能够提高开发效率,而且在提高代码可读性上也有很大的帮助.从C# 3.0开始,随着扩展方法的引入,Fluent Interface也更多地被开发人员熟悉和使用.例如,当我们希望从一个整数列表中找出所有的偶数,并将这些偶数通过降序排列的方式添加到另一个列表中时,可以使用下面的代码: 复制代码 代码如下: i.Where(p => p % 2 == 0)

  • C#中设计、使用Fluent API

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

  • FluentMybatis实现mybatis动态sql拼装和fluent api语法

    目录 开始第一个例子: Hello World 新建演示用的数据库结构 创建数据库表对应的Entity类 运行测试来见证Fluent Mybatis的神奇 配置spring bean定义 使用Junit4和Spring-test来执行测试 开始第一个例子: Hello World 新建Java工程,设置maven依赖 新建maven工程,设置项目编译级别为Java8及以上,引入fluent mybatis依赖包. <dependencies> <!-- 引入fluent-mybatis

  • C# 使用Fluent API 创建自己的DSL(推荐)

    DSL的作用是解决领域专家与软件开发人员之间的沟通问题.听起来很唬人,其实不是什么高深的东西,我们可以使用Fluent API 创建自己的DSL DSL(Domain Specified Language)领域专用语言是描述特定领域问题的语言,听起来很唬人,其实不是什么高深的东西.看一下下面的代码: using FlunetApiDemo; var 张三 = "张三" .是学生() .身高(1.62M) .体重(48M); Console.WriteLine(张三.BMI()); Co

  • Entity Framework使用Fluent API配置案例

    一.配置主键 要显式将某个属性设置为主键,可使用 HasKey 方法.在以下示例中,使用了 HasKey 方法对 Product 类型配置 ProductId 主键. 1.新加Product类 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace FluentAPI.Model { public cl

  • ASP.NET MVC5验证系列之Fluent Validation

    前面两篇文章学习到了,服务端验证,和客户端的验证,但大家有没有发现,这两种验证各自都有弊端,服务器端的验证,验证的逻辑和代码的逻辑混合在一起了,如果代码量很大的话,以后维护扩展起来,就不是很方便.而客户端的验证,必须要启用客户端验证,也就是在配置文件中配置相应的节点,并且还要引入Jquery插件.如果人为的在浏览器上,禁用了js脚本,那么客户端验证就不起作用了,所以在这里,我将继续学习另外一个验证,也就是Fluent Validation. Fluent Validation是一个开源的.NET

  • 浅谈关于JavaScript API设计的一些建议和准则

    设计是一个很普遍的概念,一般是可以理解为为即将做的某件事先形成一个计划或框架. (牛津英语词典)中,设计是一种将艺术,体系,硬件或者更多的东西编织到一块的主线.软件设计,特别是作为软件设计的次类的API设计,也是一样的.但是API设计常常很少关注软件发展,因为为其他程序员写代码的重要性要次于应用UI设计和最终用户体验. 但是API设计,作为我们自己写的库中提供的公共接口,能够向调用我们代码的开发者表现出我们库的一些特点和功能,所以API设计和UI设计一样重要.事实上,两者都是为应用可以提供更好的

  • Fluent Mybatis零xml配置实现复杂嵌套查询

    目录 嵌套查询 in (select 子查询) exists (select子查询) 嵌套查询 使用Fluent Mybatis, 不用手写一行xml文件或者Mapper文件,在dao类中即可使用java api构造中比较复杂的嵌套查询. 让dao的代码逻辑和sql逻辑合二为一. 前置准备,maven工程设置 参考文章 使用FluentMybatis实现mybatis动态sql拼装和fluent api语法 in (select 子查询) 嵌套查询表和主查询表一样的场景 .column().in

  • Fluent Mybatis如何做到代码逻辑和sql逻辑的合一

    使用fluent mybatis可以不用写具体的xml文件,通过java api可以构造出比较复杂的业务sql语句,做到代码逻辑和sql逻辑的合一.不再需要在Dao中组装查询或更新操作,在xml或mapper中再组装参数.那对比原生Mybatis, Mybatis Plus或者其他框架,FluentMybatis提供了哪些便利呢? 场景需求设置 我们通过一个比较典型的业务需求来具体实现和对比下,假如有学生成绩表结构如下: create table `student_score` ( id big

  • Fluent Mybatis,原生Mybatis,Mybatis Plus三者功能对比

    目录 三者实现对比 使用fluent mybatis 来实现上面的功能 换成mybatis原生实现效果 换成mybatis plus实现效果 生成代码编码比较 fluent mybatis生成代码设置 mybatis plus代码生成设置 FluentMybatis特性一览 三者对比总结 Fluent Mybatis介绍和源码 使用fluent mybatis可以不用写具体的xml文件,通过java api可以构造出比较复杂的业务sql语句,做到代码逻辑和sql逻辑的合一. 不用再需要在Dao中

随机推荐