文档章节

如何在自己的项目中实现 Fluent Interface(流畅接口)

tantexian
 tantexian
发布于 2016/06/12 09:07
字数 2700
阅读 95
收藏 0

如何在自己的项目中实现 Fluent Interface(流畅接口)

  布尔的许多早期工作见证了莱布尼茨对恰当的数学符号系统的力量的信念,符号似乎无需什么帮助就能奇迹般地产生出问题的正确答案,为此莱布尼茨曾举过代数的例子。在英国,当布尔开始自己的工作时,人们已经渐渐认识到代数的力量来自于这样一个事实,即代表着量和运算的符号服从不多的几条基本规则或定律。
                                            ——马丁·戴维斯 《逻辑的引擎》

  Fluent Interface 是如何让人感觉流畅的呢?也许因为它简洁、是声明式的而非命令式的、更接近自然语言的语序,而且有很好的连贯性和一致性。不管怎么说,确实有不一样的感觉。
  也许你想知道 Fluent Interface 的定义是什么(可移步维基百科),不过,最好还是通过一段示例代码来理解它。我们最近就尝试着把验证代码简单地封装成了 Fluent Interface 的形式,让我们先睹为快吧。
  假设我们有一个叫做 Student 的实体:

1

2

3

4

5

6

7

8

9

public class Student : BaseEntity

{

    // 姓名

    public virtual string Name { get; set; }

    // 学号

    public virtual string Code { get; set; }

    // 年级

    public int? Grade { get; set; }

}

在保存 Student 的业务逻辑里面,我们要进行一些验证,如果验证用的 API 具有 Fluent Interface 范儿,验证代码就会像这样:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

public class StudentBizImpl ... 

{

    public void Save(Student entity)

    {

        entity.Name.Should().UseDisplayName("姓名")

                            .NotBeNullOrEmpty();

  

        entity.Code.Should().UseDisplayName("学号")

                            .NotBeNullOrEmpty()

                            .NotBeExist("Code", entity.Id, StudentRepository.IsFieldExist);

 

        entity.Grade.Should().UseDisplayName("年级")

                             .NotBeNull()

                             .WhenHasValueShould().Between(1, 6);

        ...

    }

}

我故意一行注释都没写,你看得出验证规则是什么吗?没错,即使是不懂编程的人也差不多能猜个八九不离十:学生姓名不允许为空;学号不允许为空也不允许重复;年级不允许为空且必须在1~6之间。如果能够实现这样的验证 API,相信无论是编写验证代码还是日后阅读这些代码都会有更加舒畅的体验。

  那么如何实现它呢?其实实现方法简单得不得了。不过,在急着动手实现之前,还是让我们先明确一下设计目标吧。 


设计目标

  1. 可以使用方法链调用验证 API,并且可以“一链到底”,例如对 Grade 字段的验证,验证它不可为空以及它的值应在 1~6 之间不需要分开两段来写。
  2. 验证 API 应尽可能为声明式的,而非命令式的。
  3. 验证 API 不会对现有接口造成污染。例如,在我们键入“entity.Name.” 之后,如果 VS 的智能感知列出了一大堆诸如 “NotBeNullOrEmpty()” 之类的验证函数的话,不但看着很闹心,也容易引起误用。
  4. 静态类型检查。例如,键入“entity.Name.Should().” 之后,VS 的智能感知应该只列出字符串相关的验证,当然,如果键入“entity.Name.Should().WhenHasValueShould()” 应该引发编译期错误。
  5. 尽量使 API 可以互相独立工作,不依赖调用顺序。
  6. 尽量杜绝判断语句,使得验证代码更接近自然语言的语序。

  此外,还有一些技术上的目标:

  7. 符合 Open-Close 原则,当加入新的验证 API 时,不需要修改已有的代码(无论是 API 的实现代码还是接口)。
  8. 减少重复代码。例如对于 int、decimal、DateTime 类型都需要 Between() 这个API,能否用泛型来实现?

第一种实现:使用 XXValidator 封装验证方法,将重复代码抽取到泛型基类中

  我们首先想到的实现方法是为每一个数据类型定义一个 Validator,它有两个职责:1)实现诸如 Between() 之类的验证方法;2)持有要验证的值和显示名称等信息。我们使用名为 Validator 的抽象泛型基类封装每个具体的 Validator 都需要用到的代码:

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

public abstract class Validator<TSubClass, TValue> where TSubClass : class

{

    private TValue _value; // 字段值

    protected string _displayName; // 字段的显示名称

    protected bool _shouldValidate; // 是否进行验证。

 

    public Validator(TValue v, bool shouldValidate)

    {

        _value = v;

        _shouldValidate = shouldValidate;

    }

    /// <summary>

    /// 字段值

    /// </summary>

    public TValue Value { get { return _value; } }

    /// <summary>

    /// 字段的显示名称

    /// </summary>

    public string DisplayName { get { return _displayName; } }

    /// <summary>

    /// 是否进行验证。

    /// </summary>

    public bool ShouldValidate { get { return _shouldValidate; } }

    /// <summary>

    /// 设置字段的显示名称

    /// </summary>

    public TSubClass UseDisplayName(string displayName)

    {

        _displayName = displayName;

        return this as TSubClass;

    }

}

在 Validator 类定义一个具体子类的泛型参数 TSubClass,以及在 UseDisplayName() 里的向下转型显得很笨拙,但是为了实现方法链,似乎也没有更好的方法。接下来,我们就可以实现两个具体的 Validator 了:

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

public class Int32Validator : Validator<Int32Validator, int>

{

    public Int32Validator(int v) : this(v, true) { }

    public Int32Validator(int v, bool shouldValidate) : base(v, shouldValidate) { }

 

    public Int32Validator Between(int start, int end)

    {

        if (ShouldValidate)

            if (Value < start || Value > end)

                throw new Exception(string.Format("{0}:必须介于 {1} 和 {2} 之间!", DisplayName, start, end));

 

        return this;

    }

}

 

public class DecimalValidatior : Validator<DecimalValidatior, decimal>

{

    public DecimalValidatior(decimal v) : this(v, true) { }

    public DecimalValidatior(decimal v, bool shouldValidate) : base(v, shouldValidate) { }

 

    public DecimalValidatior Between(decimal start, decimal end)

    {

        if (ShouldValidate)

            if (Value < start || Value > end)

                throw new Exception(string.Format("{0}:必须介于 {1} 和 {2} 之间!", DisplayName, start, end));

 

        return this;

    }

}

再为每个 Validator 定义一个名为 Should() 的扩展方法作为它们的工厂:

1

2

3

4

5

6

7

8

9

10

11

12

public static class ValidatorFactory

{

    public static Int32Validator Should(this int v)

    {

        return new Int32Validator(v);

    }

 

    public static DecimalValidatior Should(this decimal v)

    {

        return new DecimalValidatior(v);

    }

}

为什么要为每个类型定义一个 Validator,而不是使用一个统一的 Validator<T> 就好了呢?这是因为我们想要达成“设计目标4”的缘故——我们希望针对特定类型的验证方法对其它类型是不可见的。但是那个 Between() 很明显是让人无法忍受的重复代码,我们可以把它们抽取到一个基类之中:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

public abstract class CompareValidator<TValidator, TValue> : Validator<TValidator, TValue>

    where TValidator : class

    where TValue : IComparable<TValue>

{

    public CompareValidator(TValue v) : this(v, true) {}

    public CompareValidator(TValue v, bool shouldValidate) : base(v, shouldValidate) { }

 

    public TValidator Between(TValue start, TValue end)

    {

        if (ShouldValidate)

            if (Value.CompareTo(start) < 0 || Value.CompareTo(end) > 0)

                throw new Exception(string.Format("{0}:必须介于 {1} 和 {2} 之间!", DisplayName, start, end));

 

        return this as TValidator;

    }

}

然后让 Int32Validator 和 DecimalValidatior 继承 CompareValidator 就可以了:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

public class Int32Validator : CompareValidator<Int32Validator, int> // Validator<Int32Validator, int>

{

    public Int32Validator(int v) : this(v, true) { }

    public Int32Validator(int v, bool shouldValidate) : base(v, shouldValidate) { }

 

    //public Int32Validator Between(int start, int end)

    //{

    //    if (ShouldValidate)

    //        if (Value < start || Value > end)

    //            throw new Exception(string.Format("{0}:必须介于 {1} 和 {2} 之间!", DisplayName, start, end));

 

    //    return this;

    //}

}

暂时看上去还不错,但这其实是错误的做法。因为在 .net 里,一个类可以实现多个接口,却只能继承自一个基类。Int32 既实现了 IComparable<int> 接口,也实现了 IEquatable<int> 接口,那么,如果我们又写了一个叫做 EqualValidator 的基类,Int32Validator 又不能同时继承 CompareValidator 和 EqualValidator,岂不呜呼哀哉?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

public abstract class EqualValidator<TValidator, TValue> : Validator<TValidator, TValue>

    where TValidator : class

    where TValue : IEquatable<TValue>

{

    public EqualValidator(TValue v) : this(v, true) { }

    public EqualValidator(TValue v, bool shouldValidate) : base(v, shouldValidate) { }

 

    public TValidator BeEqual(TValue other)

    {

        if (ShouldValidate)

            if (!Value.Equals(other))

                throw new Exception(string.Format("{0}:必须等于 {1}!", DisplayName, other));

 

        return this as TValidator;

    }

}

我们需要的是对实现代码的多继承。.net 倒是提供了一种“多继承静态方法”的功能,没错,就是扩展方法。接下来,我们要换另一种思路来实现这组验证 API。

第二种实现:自定义接口 + 扩展方法

  首先要做的,是把“持有要验证的值和显示名称等信息”的职责从 Validator 中分离出来,成为 FieldWapper 类层次:

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

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

public abstract class FieldWapper<TSubClass, TValue> where TSubClass : class

{

    private TValue _value; // 字段值

    protected string _displayName; // 字段的显示名称

    protected bool _shouldValidate; // 是否进行验证。如果此属性设置为false,针对此FieldWapper的所有验证都将失效。

 

    public FieldWapper(TValue v, bool shouldValidate)

    {

        _value = v;

        _displayName = string.Empty;

        _shouldValidate = shouldValidate;

    }

    /// <summary>

    /// 字段值

    /// </summary>

    public TValue Value { get { return _value; } }

    /// <summary>

    /// 字段的显示名称

    /// </summary>

    public string DisplayName { get { return _displayName; } }

    /// <summary>

    /// 是否进行验证。

    /// </summary>

    public bool ShouldValidate { get { return _shouldValidate; } }

    /// <summary>

    /// 设置字段的显示名称

    /// </summary>

    public TSubClass UseDisplayName(string displayName)

    {

        _displayName = displayName;

        return this as TSubClass;

    }

}

 

public class Int32Field : FieldWapper<Int32Field, int>, IComparableField<int>

{

    public Int32Field(int v) : this(v, true) { }

    public Int32Field(int v, bool shouldValidate) : base(v, shouldValidate) { }

 

    int IComparableField<int>.CompareTo(int other)

    {

        return Value.CompareTo(other);

    }

}

 

public class DecimalField : FieldWapper<DecimalField, decimal>, IComparableField<decimal>

{

    public DecimalField(decimal v) : this(v, true) { }

    public DecimalField(decimal v, bool shouldValidate) : base(v, shouldValidate) { }

 

    int IComparableField<decimal>.CompareTo(decimal other)

    {

        return Value.CompareTo(other);

    }

}

因为 int 和 decimal 都是可比较大小的,所以它们都实现了 IComparableField 接口,这个接口是我们自定义的,它很像 IComparable 接口:

1

2

3

4

public interface IComparableField<in TValue>

{

    int CompareTo(TValue other);

}

然后再以扩展方法的形式实现一个针对 IComparableField 接口的验证方法就可以了:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

public static class ComparableFieldValidator

{

    public static TField BeGreateThan<TField, TValue>(this TField field, TValue other)

        where TField : FieldWapper<TField, TValue>, IComparableField<TValue>

    {

        if (field.ShouldValidate)

            if (field.CompareTo(other) <= 0)

                throw new Exception(string.Format("{0}:必须大于 {1}!", field.DisplayName, other.ToString()));

 

        return field;

    }

 

    public static TField Between<TField, TValue>(this TField field, TValue start, TValue end)

        where TField : FieldWapper<TField, TValue>, IComparableField<TValue>

    {

        if (field.ShouldValidate)

            if (field.CompareTo(start) < 0 || field.CompareTo(end) > 0)

                throw new Exception(string.Format("{0}:必须介于 {1} 和 {2} 之间!", field.DisplayName, start, end));

 

        return field;

    }

}

同样,我们需要为每一个 FieldWapper 实现一个名为 Should() 的工厂方法:

1

2

3

4

5

6

7

8

9

10

11

12

public static class FieldWapperFactory

{

    public static Int32Field Should(this int v)

    {

        return new Int32Field(v);

    }

 

    public static DecimalField Should(this decimal v)

    {

        return new DecimalField(v);

    }

}

更多的细节请参考示例代码(VS2010 控制台应用程序)。

ps: 本想把 UseDisplayName() 也作成扩展方法,这样 FieldWrapper 的子类就不用指定那个无厘头的 TSubClass 泛型参数了,可惜,VS 提示无法推断出类型实参,让我显示指定类型实参,但是其实明明可以推断的说……

1

2

3

4

5

6

7

8

9

public static class FieldWapperExtension

{

    public static T UseDisplayName<T, TValue>(this T field, string displayName)

        where T : FieldWapper<TValue>

    {

        field.DisplayName = displayName;

        return field;

    }

}


参考

Fluent interface. wikipedia.
method chaining. wikipedia.
FluentInterface by Martin Fowler. MartinFowler.com, 2005.
Fluent NHibernate, 开源项目。
MSpec,开源项目。
TNValidate,开源项目。
Fluent Validation,开源项目。

本文转载自:

tantexian
粉丝 225
博文 527
码字总数 746616
作品 0
成都
架构师
私信 提问
这也是C#代码吗 --- 代码阅读性进阶:测试文档化

没有太多的罗嗦,代码本身已经足够。如果,要添几个标签的话就是: 中文化,流畅性接口(Fluent Interface),API. 只有几点补充说明: 这都是真实可运行的代码 测试使用Machine Specificati...

予沁安
2012/12/25
232
0
在C#中使用装饰器模式和扩展方法实现Fluent Interface

在C#中使用装饰器模式和扩展方法实现Fluent Interface 背景知识 Fluent Interface是一种通过连续的方法调用以完成特定逻辑处理的API实现方式,在代码中引入Fluent Interface不仅能够提高开发...

tantexian
2016/06/12
103
0
UWP 流畅设计中的光照效果(容易的 RevealBorderBrush 和不那么容易的 RevealBackgroundBrush)

在 Windows 10.0.16299 中,RevealBrush 被引入,可以实现炫酷的鼠标滑过高亮效果和点击光照。本文将告诉大家如何完整地实现这样的效果。 Reveal 的效果(自带) 在微软官方推荐的 XAML Con...

wpwalter
2018/04/15
0
0
流畅设计 Fluent Design System 中的光照效果 RevealBrush,WPF 也能模拟实现啦!

UWP 才能使用的流畅设计效果好惊艳,写新的 UWP 程序可以做出更漂亮的 UI 啦!然而古老的 WPF 项目也想解解馋怎么办? 于是我动手实现了一个! 迫不及待看效果 ▲ 是不是很像 UWP 中的 ? 不...

wpwalter
2018/04/05
0
0
Golang资料集

该资源的github地址:Qix 《Platform-native GUI library for Go》 介绍:跨平台的golang GUI库,支持Windows(xp以上),Unix,Mac OS X(Mac OS X 10.7以上) 《Gopm 快速入门》 介绍:Gopm(Go 包管...

ty4z2008
2016/03/11
0
0

没有更多内容

加载失败,请刷新页面

加载更多

arduino项目-1. 模拟楼道灯

@toc 1.1 情景说明 说明 漆黑的夜晚,当有人非法进入一所房屋,房屋内的灯在恰当的时间亮起,也许会有效阻止非法活动的继续。 效果展示 1.2 实验器材 器材名称 数量 继电器 1 人体红外感应器...

acktomas
28分钟前
4
0
Nacos 常见问题及解决方法

Nacos 开源至今已有一年,在这一年里,得到了很多用户的支持和反馈。在与社区的交流中,我们发现有一些问题出现的频率比较高,为了能够让用户更快的解决问题,我们总结了这篇常见问题及解决方...

阿里云官方博客
34分钟前
6
0
pinyin4j 满足中文转拼音的需求

引入依赖 // https://mvnrepository.com/artifact/com.belerweb/pinyin4j //汉字转拼音compile group: 'com.belerweb', name: 'pinyin4j', version: '2.5.1' 写入中文转拼英的工具......

edison_kwok
39分钟前
5
0
IPSE接入Substrate/Polkadot插槽实现互操作性的运行原理

Substrate框架将区块链的众多功能都模块化,对于开发者来说,只是一个选择的问题,同时还保持了众多的可以定制的功能和模块,比如底层通信模块,比如账户体系,比如共识机制等都是可以自己定...

IPSE
45分钟前
156
0
linux配置安装phpMyAdmin的步骤记录

1、首先在phpMyAdmin官方网站 http://www.phpmyadmin.net/downloads下载源码包,或者通过脚本之家进行下载://www.jb51.net/codes/405261.html ,下载后上传到服务器解压即可,或者通过Linux...

蜗牛女孩
46分钟前
6
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部