API设计的一些心得总结

  你作过API设计么?无论你是否作过API设计,都不妨看看老赵的这篇博文。在这篇文章中,老赵总结了本身进行API设计的一些心得。html

  我平时的主要工做之一,即是编写一些基础及通用的类库,可以在项目中大量复用。换句话说,个人工做目的,是让其余开发人员能够更好地完成工做。所以,如何设计更容易使用的API是我常常要考虑的东西,偶尔也会有一些体会。而如今这些内容,是我在为Functional Reactive Programing写“参考答案”的时候突然“总结”出来的想法。可能比较简单,但我想也是设计API是须要考虑的一些内容。程序员

  在那篇文章里,咱们是在为IEvent< T>对象提供一些封装,其中会有MapEvent和FilterEvent等类型,为了方便调用,咱们还定义了对应的扩展方法:app

 
  1. public class MapEvent< TIn, TOut> : InOutEventBase< TIn, TOut>  
  2. {  
  3.     public MapEvent(Func< TIn, TOut> mapper, IEvent< TIn> inEvent)  
  4.         : base(inEvent)  
  5.     {  
  6.         ...  
  7.     }  
  8. }  
  9.  
  10. public class FilterEvent< TEventArgs> : InOutEventBase< TEventArgs, TEventArgs>  
  11. {  
  12.     public FilterEvent(Func< TEventArgs, bool> predicate, IEvent< TEventArgs> inEvent)  
  13.         : base(inEvent)  
  14.     {  
  15.         ...  
  16.     }  
  17. }  
  18.  
  19. public static class EventExtensions  
  20. {  
  21.     public static MapEvent< TIn, TOut> Merge< TIn, TOut>(  
  22.         this IEvent< TIn, TOut> ev, Func< TIn, TOut> mapper)  
  23.     {  
  24.         ...  
  25.     }  
  26.  
  27.     public static FilterEvent< TEventArgs> Filter< TEventArgs>(  
  28.         this IEvent< TEventArgs> ev, Func< TEventArgs, bool> predicate)  
  29.     {  
  30.         ...  
  31.     }  
  32. }  
  33.  

  MergeEvent和FilterEvent都是对另外一个Event对象的封装,您能够看成一种装饰器模式来考虑。不知您观察到没有,这个“待封装”的Event对象在不一样的地方(构造函数或扩展方法),出现的位置是不一样的。在扩展方法中,它是做为第一个参数出如今参数列表中,而在构造函数中它则是第二个参数。对于扩展方法来讲,它是由语言规范强制得出的。可是在构造函数中,这出现的顺序彻底可有由咱们“自由”肯定。那么,咱们可否将待封装的Event对象做为构造函数的第一个参数呢?框架

  天然是能够的,只是我在这里倾向于放在最后。缘由在于这有利于API使用时的清晰。dom

  假如咱们没有扩展方法,也就是说只能使用构造函数进行“装饰”,那么使用如今则是:函数

 
  1. var ev =  
  2.     new MapEvent< intstring>(  
  3.         i => i.ToString(),  
  4.         new FilterEvent< int>(  
  5.             i => i <  10,  
  6.             new MapEvent< DateTime, int>(  
  7.                 d => d.Millisecond,  
  8.                 ...)));  
  9.  

  有的时候,我会将Lambda表达式写在上一行,这样可让代码更为紧凑。那么若是MapEvent和FilterEvent都把待封装的Event对象做为构造和函数的第一个参数,又会怎么样呢?ui

 
  1. var ev =  
  2.     new MapEvent< intstring>(  
  3.         new FilterEvent< int>(  
  4.             new MapEvent< DateTime, int>(  
  5.                 ...,  
  6.                 d => d.Millisecond),  
  7.             i => i <  10),  
  8.         i => i.ToString());  
  9.  

  对比这二者,在我看来它们的信息“呈现方式”是有显著差距的。对于第一种状况(Event做为构造函数最后一个参数),用户看到这个定义时,从上到下的阅读顺序是:this

  1. 构造一个MapEvent对象,映射方式是XXX
  2. 包含一个FilterEvent对象,过滤条件是YYY
  3. 包含一个MapEvent对象,映射方式是ZZZ

  而对于第二种状况(Event做为构造函数的第一个参数):spa

  1. 构造一个MapEvent对象
  2. 包含一个FilterEvent对象
  3. 构造一个MapEvent对象
  4. 最内层MapEvent的映射方式为ZZZ
  5. 上一层FiterEvent……
  6. ……

  第一种状况,API体现出的信息是流畅的,而第二种状况信息的体现是回溯的。第一种信息如“队列”,而第二种如“栈”。第一种API阅读起来用户视线是单向的,而第二种API用户可能会去努力寻找某个Lambda表达式到底对应着哪一个对象——就像咱们为何提倡if/for不该该嵌套太深,由于找匹配的大括号的确是件比较麻烦的事情。我想,应该没有会选择把Event对象放在构造函数参数列表的中间吧(若是有3个及参数),由于这会让API调用看起来成“锯齿状”,实在不利于阅读。设计

  所以,在各类须要“装饰”的场合,我每每都把“被装饰者”做为构造函数的最后一个参数。例如我在构造DomainRoute的时候,便也是把innerRoute做为构造函数的最后一个参数,因为DouteRoute所须要的参数较多,所以若是把innerRoute做为第一个参数,看起来会更加不便一些。一样的,在以前设法“拯救C# 2.0”的时候也使用了这个作法。

  固然,这些是我我的的见解,并不是全部人都是这样作的。例如在.NET Framework中负责GZip压缩的GZipStream对象,它的构造函数即是将innerStream做为第一个参数出现。幸亏,C# 3.0中已经有了扩展方法,若是使用构造函数的话,即便信息再流畅,我想也不如扩展方法来的直观。所以,我通常都会利用扩展方法,让开发人员能够编写这样的API:

 
  1. dateEvent.Map(d => d.Millisecond).Filter(i => i <  10).Map(i => i.ToString())  
  2. route.WithDomain("http://www.{*domain}/blogs"new { ... });  
  3. stream.GZip(CompressionMode.Compress).Encrypt(...);  
  4.  

  其实许多高级语言都会为了让代码写的更易懂更清晰,于是提供一些看似“语法糖”的东西。例如F#中的|>操做符:

 
  1. let form = new Form(Visible = true, TopMost = true, Text = "Event Sample")  
  2. form.MouseDown  
  3.     |> Event.merge form.MouseMove  
  4.     |> Event.filter (fun args -> args.Button = MouseButtons.Left)  
  5.     |> Event.map (fun args -> (args.X, args.Y))  
  6.     |> Event.listen (fun (x, y) -> printfn "(%d, %d)" x y)  
  7.  

  其实|>操做符的目的只是把函数的最后一个参数调到以前来,但它能让咱们写出“易读”的代码。例如FsTest类库容许咱们这样写:

 
  1. "foo" |> should equal "foo" 
  2.  

  但其实,从理论上说,这种写法彻底等价于:

 
  1. should equal "foo" "foo" 
  2.  

  正是由于有了|>操做符,F#在这种状况下会将待封装的Event对象做为函数的最后一个参数。这即是语言特性对API设计的影响。此外,F#中的“>>”以及Haskell的“.”可用“`”把一个函数做为中缀操做符来使用。但若是是Java这样的语言,因为缺少一些灵活的语法特性,开发人员就只能靠框架和类库来构建“Fluent Interface”来度过难关了(如Google Collections)。《卓有成效的程序员》一书中举了这么一个例子,它们为一个Car对象的构造编写了流畅接口:

 
  1. Car car = Car.describedAs().  
  2.              .box()  
  3.              .length(50.5)  
  4.              .type(Type.INSULATED)  
  5.              .includes(Equipment.LADDER)  
  6.              .lining(Lining.CORK);  
  7.  

  以代替呆板的Java语法:

 
  1. Car car = new CarImpl();  
  2. MarketingDescription desc = newMarketingDescriptionImpl();  
  3. desc.setType("Box");  
  4. desc.setSubType("Insulated");  
  5. desc.setAttribute("length""50.5");  
  6. desc.setAttribute("ladder""yes");  
  7. desc.setAttribute("lining type""cork");  
  8. car.setDescription(desc)  
  9.  

  彷佛程序员永远不会放弃这方面追求:编写更清晰,更易懂的代码。