布尔的许多早期工作见证了莱布尼茨对恰当的数学符号系统的力量的信念,符号似乎无需什么帮助就能奇迹般地产生出问题的正确答案,为此莱布尼茨曾举过代数的例子。在英国,当布尔开始自己的工作时,人们已经渐渐认识到代数的力量来自于这样一个事实,即代表着量和运算的符号服从不多的几条基本规则或定律。
——马丁·戴维斯 《逻辑的引擎》
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,开源项目。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· 字符编码:从基础到乱码解决
· Open-Sora 2.0 重磅开源!