开发人员通常都会选择最熟悉的语言特性来描述组件之间的契约,对于大多数开发者来说,一般会使用基类或接口来定义其他类型所需要的方法,然后根据这些接口编写代码,通常来说这没什么问题,不过使用函数参数能够让其他开发者在使用你的组件和类库时更容易些。使用函数参数意味着你的组件无需负责提供类型所需的具体处理逻辑,而是将其抽象出来,交给调用者实现。
我们都已经熟悉了通过接口或抽象类来实现分离,不过有些时候,定义并实现接口仍旧显得过于笨重,虽然这样做复合了传统面向对象的理念,不过其他的技术则可以实现更简单的API。使用委托一样可以创建契约,同时也降低了客户的代码量。我们的目的是尽可能的将你的工作与客户使用者代码分离,降低两者之间的依赖,如果不这样做的话,会给你和你的使用者带来不少的困难。你的代码越是依赖其他代码,就越难以单元测试或在其他地方重用。从另一方面考虑,你的代码越是需要客户代码遵守某类特定的模式,那么客户也会承受越多的约束。
使用函数参数即可降低你的代码与其他使用者的偶合程度。如下示例:.Net内部类List<T>中
List<T>.RemoveAll()方法就接受了一个委托类型PrePreDicate<T>,当然.Net的设计者也可以通过定义接口来实现该方法。
//带来不恰当的额外耦合 public interface IPredicate<T> { bool Match(T soughtObject); } public class List<T> { public int RemoveAll(IPredicate<T> match) { //省略 return 0;//共移除多少条记录 } //其他API省略 } //第二个版本的使用方式就有些复杂 public class MyPredicate : IPredicate<int> { public bool Match(int soughtObject) { return soughtObject < 100; } }
通过对比两者,使用委托等更松散的方式来定义契约的话,那么其他开发者使用起来将会更加容易。之所以用委托而不是接口定义契约,是因为委托并不是类型的基本属性。这与方法的个数无关——很多.Net Framework中的接口都包含一个方法,例如IComparable<T>和IEquatable<T>等都是不错的定义,实现这些接口意味着你的类型拥有了某些特定的属性,支持相互之间进行比较或相等性的判断。不过实现了这个假定的IPredicate<T>却并没有说明类型的特定属性,对于那些单个API来说,定义一个方法就足够了。
通常,在你开始考虑使用基类或接口时,可以考虑使用函数参数与泛型方法来配合使用,如下给出一个Concat示例。第一个方法为普通的序列拼接,第二个使用了泛型方法与函数参数来构造输出序列
/// <summary> /// 对2个序列的每个元素进行拼接 /// </summary> public static IEnumerable<string> Concat(this IEnumerable<string> first, IEnumerable<string> second) { using (IEnumerator<string> firstSequence = first.GetEnumerator()) { using (IEnumerator<string> sencodSequence = second.GetEnumerator()) { while (firstSequence.MoveNext() && sencodSequence.MoveNext()) { yield return string.Format("{0} {1}", firstSequence.Current, sencodSequence.Current); } } } } /// <summary> /// 根据指定委托joinFunc,对2个序列的每个元素进行拼接 /// </summary> public static IEnumerable<TResult> Concat<T1, T2, TResult>(this IEnumerable<T1> first, IEnumerable<T2> second, Func<T1, T2, TResult> joinFunc) { using (IEnumerator<T1> firstSequence = first.GetEnumerator()) { using (IEnumerator<T2> sencodSequence = second.GetEnumerator()) { while (firstSequence.MoveNext() && sencodSequence.MoveNext()) { yield return joinFunc(firstSequence.Current, sencodSequence.Current); } } } }
随后调用者必须给出joinFunc的实现,如下,这样就更加降低了Concat方法与调用者之间的耦合。
IEnumerable<string> result = CombinationSequence.Concat(first, second, (one, two) => string.Format("{0} {1}", one, two));
有些时候,我们想要在序列的每个元素上执行某个操作,最后返回一个汇总的数据。例如,下面的3个方法都将统计序列中所有整数的和,但三个方法都有差别,
1)第一个为一般性的统计,只能统计int类型,统计的累加方式为固定的。
2)第二个是针对一个方法进行了抽象,改成了泛型累加器,将Sum算法提取出来用一个委托来代替。那么就可以统计任意类型的数据,累加的计算方式可以自己定义。
3)第三个是针对第二个进行修改,因为第二个写法中,Sum仍旧有不少限制,其必须使用与序列中的元素、返回值、初始值同样的类型,而我们可能需要使用一些不同的类型。所以可以对Sum方法进行少量的修改,允许序列中的元素与累加的结果使用不同的类型。
public static int Sum(IEnumerable<int> nums) { int total = 0; foreach (var num in nums) { total += num; } return total; } public static T Sum<T>(this IEnumerable<T> sequence, Func<T, T, T> accumulator) { T total = default(T); foreach (var item in sequence) { total = accumulator(total, item); } return total; } public static TResult Sum<T, TResult>(this IEnumerable<T> sequence, Func<TResult, T, TResult> accumulator) { TResult total = default(TResult); foreach (var item in sequence) { total = accumulator(total, item); } return total; }
使用函数参数能够很方便的将算法与特定的数据类型分离。若你的对象保存了传入的委托以备稍后调用,那么这个对象就控制了委托中对象的生命周期,这就可能延长了此类对象的生命周期,这和先让一个对象引用另一个对象(通过存放对接口或基类的引用),然后再使用的情况没什么不同,但在阅读代码时更难以发现。
在定义组件与其他客户代码的通信契约时,默认的选择仍然是接口。抽象基类则能提供一些默认的公共实现,让客户代码无需重复编写,而为方法定义委托则提供了最大的灵活性,但也意味着你得到的支持会更少,总的来讲,就是用更多的工作换来更好的灵活性。