API设计的一些心得总结

开发 开发工具
如何设计更容易使用的API?对于不同的语言,这个答案是不一样的,因为不同语言有着不同的语言特性。本文总结了一些API设计时需要考虑的事项,其中包括语言特性的影响。

你做过API设计么?不管你是否做过API设计,都不妨看看老赵的这篇博文。在这篇文章中,老赵总结了自己进行API设计的一些心得。

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

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

public class MapEvent< TIn, TOut> : InOutEventBase< TIn, TOut>  
{  
    public MapEvent(Func< TIn, TOut> mapper, IEvent< TIn> inEvent)  
        : base(inEvent)  
    {  
        ...  
    }  
}  
 
public class FilterEvent< TEventArgs> : InOutEventBase< TEventArgs, TEventArgs>  
{  
    public FilterEvent(Func< TEventArgs, bool> predicate, IEvent< TEventArgs> inEvent)  
        : base(inEvent)  
    {  
        ...  
    }  
}  
 
public static class EventExtensions  
{  
    public static MapEvent< TIn, TOut> Merge< TIn, TOut>(  
        this IEvent< TIn, TOut> ev, Func< TIn, TOut> mapper)  
    {  
        ...  
    }  
 
    public static FilterEvent< TEventArgs> Filter< TEventArgs>(  
        this IEvent< TEventArgs> ev, Func< TEventArgs, bool> predicate)  
    {  
        ...  
    }  
}  
 
  • 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.

MergeEvent和FilterEvent都是对另一个Event对象的封装,您可以当作一种装饰器模式来考虑。不知您观察到没有,这个“待封装”的Event对象在不同的地方(构造函数或扩展方法),出现的位置是不同的。在扩展方法中,它是作为第一个参数出现在参数列表中,而在构造函数中它则是第二个参数。对于扩展方法来说,它是由语言规范强制得出的。但是在构造函数中,这出现的顺序完全可有由我们“自由”确定。那么,我们能否将待封装的Event对象作为构造函数的第一个参数呢?

自然是可以的,只是我在这里倾向于放在最后。原因在于这有利于API使用时的清晰。

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

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

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

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

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

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

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

  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:

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

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

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

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

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

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

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

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

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

以代替呆板的Java语法:

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

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

【编辑推荐】

  1. C# 3.0新语言特性浅析
  2. 利用Visual C#和C#语言特性
  3. Jython入门指导:语言特性简介
  4. 比较几个JDK版本的新增语言特性
  5. 哪种语言将统治多核时代 再看函数式语言特性
责任编辑:yangsai 来源: 博客园
相关推荐

2011-09-19 10:15:10

移动界面设计

2014-08-14 09:25:31

Linux串口

2011-06-16 20:35:34

SEO

2011-07-13 09:13:56

Android设计

2015-12-08 09:05:41

Java内部类

2011-07-12 09:47:53

WebService

2009-06-22 15:36:00

如何学好java

2011-11-28 15:57:26

MySQL数据库主从配置

2009-07-15 16:16:22

JDBC下载

2009-11-25 10:08:41

Cisco无线路由

2013-12-24 14:50:39

Ember.js框架

2020-05-19 14:35:42

Shell脚本循环

2012-07-03 13:55:00

ASP.NET

2020-04-10 08:50:37

Shell脚本循环

2012-06-07 10:17:55

软件设计设计原则Java

2009-11-26 10:32:57

PHP代码优化

2020-09-28 06:45:42

故障复盘修复

2019-08-19 14:56:07

设计模式javascript

2012-01-09 16:02:19

JavaJVM

2020-10-19 19:25:32

Python爬虫代码
点赞
收藏

51CTO技术栈公众号