How to Design a Good API and Why it Matters
近日设计RMI到Infiniband网络的接口,发现如何设计一个好的API是个很有挑战的问题。看了Joshua Bloch的ppt,做了一个总结
API可以成为一个公司很大的资产,因为好的API是可以赢得客户的。而且一旦API被public就很难改变,这也是在Java的包中有很多Deprecated的类和方法,却不能直接被删除。
好的代码应该是模块化的,每个模块都有自己的API;
有用的模块往往是很容易重用的;
对API的设计仔细考虑可以提高代码质量;
好的API的特点:
• Easy to learn
• Easy to use, even without documentation
• Hard to misuse
• Easy to read and maintain code that uses it
• Sufficiently powerful to satisfy requirements
• Easy to extend
• Appropriate to audience
API设计的流程
收集需求-带有一定程度的怀疑
一个简短的规范,这时不要求完备,但是要敏捷;
让尽量多的人参与讨论,听取别人的看法
充实这个规范,一遍coding,一边完善规范
基本规则
功能应该容易解释,设计好的命名,使得可以很容易地进行模块的划分和合并
API应该尽量小,但是不能太小;API只能添加不能删除,合理大小的API要比bulk的API要好,所以应该尝试找到好的 功能体积的比率
实现不能影响API,区分出什么是实现的细节,不要太详细指定方法的行为,一些调优的参数都是应该怀疑的;不要让实现的细节困扰用户,同时也要现在能够改变实现的自由
使任何事情的可访问度都尽量的小,尽量让class和member是private的,public的类不能有public的field,编译时常量除外;这样可以使信息隐藏达到最大,同时允许模块可以独立地被使用,修改,调试和测试。
命名;名字要尽量可以自解释;要有一致性,同样的名字意味着同样的东西,包括整个API,甚至API直接都要保持。尽量保持对称,代码看起来如同散文一样
写好文档。好的API设计和好的文档是实现代码重用的重要技术。
为每个类、接口、方法、构造函数、参数和异常加注释。类要表明一个实例代表什么;方法要描述清楚方法和客户之间的契约,包括前验条件、后验条件和副作用,参数要指明单位、表单和所有权;仔细的对状态空间写好文档。
不好的设计会影响性能:尽量多使用不可变对象;使用静态的工厂方法而不使用构造函数;使用接口而不是具体实现的类
不要歪曲了API来获得好的性能,性能提高了,可能以后会很痛苦,性能应该与设计良好的API保持一致。
Component.getSize()返回一个可变的Dimension对象,这样每个getSize方法都要分配一个Dimension对象,造成了大量无用的Dimension对象分配。在1.2中引入了新的方案(which?)
• Do what is customary
Obey standard naming conventions
Avoid obsolete parameter and return
Mimic patterns in core APIs and language
• Take advantage of API-friendly features
Generics, varargs, enums, default arguments
• Know and avoid API traps and pitfalls
Finalizers, public static final arrays
类设计
最小化可变类的使用。类应该都是不可变的,除非有足够的理由让他可变,这样实现是简单的、线程安全并可重用
如果是可变类,就应该严格限制状态空间的大小,并预先定义好。Date和Calender设计的问题和TimerTask的成功经验
只有在很讲的通的情况下才子类化,因为子类化(继承)意味着可以代换(里氏代换原则),public类不应该为了实现的容易而去继承其他的public类。反面例子Properties、Stack正面例子Set
如果一定要继承,就要做好设计和文档,否则就不准继承,继承后,子类可以看到超类的实现细节,破坏了封装。保守的观点说,所有的类都应该是final的。正面例子AbstractSet, AbstractMap
方法设计
不要让客户做模块应该完成的事情
尽量减少令人惊奇的事情。API使用者不能对API的行为有惊讶,即使减少性能也是可以接受的。
快速失败的API,应该在发生时尽量快的报告错误。最好能在编译的时候报告错误。在运行时,第一个不好的方法调用是最好的。
让程序能够对所有可用数据以String的形式可以访问,不然,客户就要对很长的字符串进行解析。
在重载时要特别留意,多个重载函数不应应用同样的参数,保守派认为尽量不要有同样个数个参数的重载函数
使用合适的参数和返回类型。使用接口而不是使用类可以提高灵活性和性能;使用最具体的可能输入参数类型,将错误从运行时转移到编译时;如果有其他的类型就不要使用String类型,String很麻烦,容易出错而且很慢(?),不要使用浮点数标识Monetary,会造成差错;使用double而不是float,可以保证精确度。
在方法中使用一致的参数调用顺序。
避免使用过长的参数列表,小于等于3个参数是很好的,多了会让用户不得不去参考doc,可以把函数拆小或者将所有的参数放在一个类中,作为参数传递;
避免返回需要参数处理的值,比如,可以返回长度为0的数组或者collection,而不要返回null。
异常设计
不要强迫用户使用异常作为控制流的一部分,但是,也不能在fail时不报告异常;要注意用好checked Exception,避免unchecked Exception的过度使用(不可能发生异常的地方不要catch,也不要throw);
在异常中包含可以用于捕获异常的信息,可以用来诊断、修复或者恢复,对于unchecked Exception可以附加信息,对checked Exception,应该提供访问接口。
重构API设计
参考资料:How to Design a Good API and Why it Matters, Joshua Bloch