Google首席软件工程师Joshua Bloch谈如何设计一款优秀的API【附PPT】

编者按】随着近来软件规模的日益庞大,API编程接口的设计变的愈来愈重要。良好的接口设计能够下降系统各部分之间的相互依赖,提升组成单元的内聚性,下降组成单元间的耦合度,从而提升系统的维护性和稳定性。html

Joshua Bloch是美国著名程序式设计师。他为Java平台设计并实现了许多的功能,是Google的首席Java架构师(Chief Java Architect)。他也是《Effective Java Programming Language Guide》一书的做者,就是人们常说的Effective Java。本文翻译自Joshua Bloch所发表的一个PPT: How to Design a Good API and Why it Mattersjava

随着大数据、公共平台等互联网技术的日益成熟,API接口的重要性日益凸显,从公司的角度来看,API能够算做是公司一笔巨大的资产,公共API能够捕获用户、为公司作出许多贡献。对于我的来讲,只要你编程,你就是一个API设计者,由于好的代码便是模块——每一个模块即是一个API,而好的模块会被屡次使用。此外,编写API还有利于开发者提升代码质量,提升自身的编码水平。程序员

优秀API所具有的特征编程


 

  • 简单易学;
  • 易于使用,即便没有文档;
  • 很难误用;
  • 易于阅读,代码易于维护;
  • 足够强大,能够知足需求;
  • 易于扩展;
  • 适合用户。

 

了解了一款优秀API所具有的特征后,一块儿再来看看如何设计优秀的API,有哪些流程和规则可循,开发者在设计时须要注意哪些事项。api

API设计流程中的注意事项数组


征集需求 架构

在开始以前,你可能会收到一些解决方案,它们不必定会比现有的方案好,而你的任务是以用例的形式提取真实需求,并制定真正合适的解决方案,这样构建出来的东西就会更加有价值。app

从简短的说明开始dom

这时,编写简短的说明最为合适,编写时须要考虑的因素有:ide

 

  • 灵活性要远胜于完整性;
  • 跳出规则:听取意见并严阵以待;
  • 精炼短小才易修改;
  •  得到信任以后将其具体化,在此之中,编程很重要。

 

 

 

尽早编写API

 

  • 对每个实现进行保存,以防丢失;
  • 在开始以前,列出一些合理的规定,保存所写说明,以防丢失;
  • 继续编写和充实API。

 

 

 

编写SPI尤其重要

 

  • Service Provider Interface即服务提供商接口,插件服务支持多种实现,例如Java Cryptography Extension (JCE);
  • 发布以前编写多个插件;
  • “三次原则”(“The Rule of Threes”):指的是当某个功能第三次出现时,才进行"抽象化"。

 

 

维护切实可行的指望

 

 

  • 大多数API设计都过于约束;
  • 对可能会犯的错误进行预计,要用发展的思惟来编写API。

 

API设计原则


每一个API接口应该只专一一件事,并作好:若是它很难命名,那么这或许是个很差的征兆,好的名称能够驱动开发、而且只需拆分与合并模块便可

 

  • API应尽量地轻小:知足需求、对有疑问的地方能够暂时不使用(函数、类、方法、参数等,你能够不添加,但千万不要删除)、概念性的东西比体积重要、寻找一个良好的动力体积比;
  • 实现不要影响API:关注实现细节(不要迷惑用户、不要随便改变实现方式)、意识到具体的实现细节(不要有越权的方法行为,例如不要制订哈希函数、全部的调优参数都是可疑的);
  • 不要让实现细节“泄露”到API(例如on-disk和on-the-wire格式等异常状况);
  • 最小化可访问:设计人员应尽可能把类及成员设为私有,公共类不该该有公共字段(包括异常实例),最大限度地提升信息隐藏,容许模块能够被使用、理解、构建、测试和独立调试;
  • 命名问题:应该见名知意,避免含糊的缩写、对同同样东西的命名应该有个一致性的前缀(遍布整个平台API)、讲究对称、代码应该易读。以下所示:

 

 

[java]  view plain copy 在CODE上查看代码片 派生到个人代码片
 
  1. if (car.speed() > 2 * SPEED_LIMIT)  
  2.  generateAlert("Watch out for cops!");  

重视文档

开发API时要意识到文档的重要性。组件重用不是纸上谈兵的东西,既须要好的设计,也须要优秀的文档,这两者缺一不可,即便咱们看到了良好的设计而未见文档,那么组件重用也是不妥的。

——摘自 D. L. Parnas 在1994年第16届国际软件开发大会上的演讲内容

文档应包含每一个类、接口、方法、构造函数、参数和异常,此外,还要当心对待文档的状态空间。

API设计决策对性能的影响

 

  • API设计决策对性能的影响是真实永久的;
  • 很差的决策会限制性能(类型易变、构造函数替代静态工厂、实现类型替代接口);
  • 不得打包API来提高性能(潜在的性能问题可能会获得修复,但救的了一时,救不了一世);
  • 良好的设计一般与好的性能是一致的。

 

API与平台和平共处

 

  • 养成良好的习惯:遵照标准的命名约定、避免陈旧的参数和返回类型、核心API和语言的模仿模式;
  • 利用API的友好功能:泛型、可变参数、枚举、默认参数;
  • 了解和避免API陷阱和缺陷:Finalizers、公共静态Final数组。

 

API中类的设计


最小化可变性

 

  • 最好不要随便改变类,除非有一个很是合理的理由;
  • 若是是可变类,最好保持很小的状态空间、定义良好的结构,因时制宜地去调用方法。

 

子类只存在有意义的地方

 

  • 子类具有可替代性(Liskov);
  • 公共类不该该继承其它公共类。

 

用于继承的设计和文档或者直接禁止继承(Design and Document for Inheritance or Else Prohibit it

 

  • 继承破坏封装
  • 若是你容许子类和文档自用,那么要考虑彼此该如何互相调用方法
  • 保守策略:把全部类都设置成Final

 

API中的方法设计


模块能作到的,客户端就不要作

 

 

减小模板代码的使用: 

 

[java]  view plain copy 在CODE上查看代码片 派生到个人代码片
 
  1. import org.w3c.dom.*;  
  2.  import java.io.*;  
  3.  import javax.xml.transform.*;  
  4.  import javax.xml.transform.dom.*;  
  5.  import javax.xml.transform.stream.*;  
  6.  // DOM code to write an XML document to a specified output stream.  
  7.  private static final void writeDoc(Document doc, OutputStream out)throws IOException{  
  8.  try {  
  9.  Transformer t = TransformerFactory.newInstance().newTransformer();  
  10.  t.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, doc.getDoctype().getSystemId());  
  11.  t.transform(new DOMSource(doc), new StreamResult(out));  
  12.  } catch(TransformerException e) {  
  13.  throw new AssertionError(e); // Can’t happen!  
  14.  }  
  15.  }  

遵照最小惊讶原则

 

 

用户API只需根据需求来设计便可,没必要让客户感到惊讶,当心弄巧成拙: 

 

[java]  view plain copy 在CODE上查看代码片 派生到个人代码片
 
  1. public class Thread implements Runnable {  
  2.  // Tests whether current thread has been interrupted.  
  3.  // Clears the interrupted status of current thread.  
  4.  public static boolean interrupted();  
  5.  }  

故障快速报告应尽快生成

 

 

 

  • 编译时最好是静态类型、泛型;
  • 方法里应该包含错误自动提交机制。

 

 

[java]  view plain copy 在CODE上查看代码片 派生到个人代码片
 
  1. // A Properties instance maps strings to strings  
  2.  public class Properties extends Hashtable {  
  3.  public Object put(Object key, Object value);  
  4.  // Throws ClassCastException if this properties  
  5.  // contains any keys or values that are not strings  
  6.  public void save(OutputStream out, String comments);  
  7.  }  

以String形式对全部可用数据提供编程式访问

 

 

 

[java]  view plain copy 在CODE上查看代码片 派生到个人代码片
 
  1. public class Throwable {  
  2.  public void printStackTrace(PrintStream s);  
  3.  public StackTraceElement[] getStackTrace(); // Since 1.4  
  4. }  
  5. public final class StackTraceElement {  
  6.  public String getFileName();  
  7.  public int getLineNumber();  
  8.  public String getClassName();  
  9.  public String getMethodName();  
  10.  public boolean isNativeMethod();  
  11. }  

方法重载要细心

 

 

  • 避免模棱两可的重载,例如多个重载适用于同一个实物
  • 即便你能分清,也最好不要这样作,最好起个不一样的名字
  • 若是非要定义这种重载,相同的参数确保相同的行为

 

 

[java]  view plain copy 在CODE上查看代码片 派生到个人代码片
 
  1. public TreeSet(Collection c); // Ignores order  
  2. public TreeSet(SortedSet s); // Respects order  

使用合适的参数和返回类型

 

  • 经过类来支持接口类型输入
  • 尽量地使用最特定的输入参数类型
  • 若是已经有一个更好的类型存在,就不要使用string类型
  • 不要用浮点型来修饰货币值
  • 使用Double(64位)而不要使用Float(32位)
  • 在方法上参数顺序要一致,尤为是参数类型相同时,则尤其重要

 

[cpp]  view plain copy 在CODE上查看代码片 派生到个人代码片
 
  1. #include <string.h>  
  2.  char *strcpy (char *dest, char *src);  
  3.  void bcopy (void *src, void *dst, int n);  
java.util.Collections – first parameter always collection to be modified or queried 
 java.util.concurrent – time always specified as long delay, TimeUnit unit

避免使用长参数列表

 

 

 

  • 三个或三个之内的参数是最完美的
  • 长参数列表是有害的,程序员容易出错,而且程序在编译、运行时会表现很差
  • 缩短参数的两种方法:Break up method、建立参数助手类

 

最好避免这种状况出现:

 

[java]  view plain copy 在CODE上查看代码片 派生到个人代码片
 
  1. // Eleven parameters including four consecutive ints  
  2. HWND CreateWindow(LPCTSTR lpClassName, LPCTSTR lpWindowName,  
  3.  DWORD dwStyle, int x, int y, int nWidth, int nHeight,  
  4.  HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam);  

返回值勿需进行异常处理

好比,返回零长度字符串或者空集合

 

[java]  view plain copy 在CODE上查看代码片 派生到个人代码片
 
  1. package java.awt.image;  
  2.  public interface BufferedImageOp {  
  3.  // Returns the rendering hints for this operation,  
  4.  // or null if no hints have been set.  
  5.  public RenderingHints getRenderingHints();  
  6.  }  

API中的异常设计

 


 

抛出异常来讲明异常情况;不要强迫客户端使用异常来控制流。

 

[java]  view plain copy 在CODE上查看代码片 派生到个人代码片
 
  1. private byte[] a = new byte[BUF_SIZE];  
  2.  void processBuffer (ByteBuffer buf) {  
  3.  try {  
  4.  while (true) {  
  5.  buf.get(a);  
  6.  processBytes(tmp, BUF_SIZE);  
  7.  }  
  8.  } catch (BufferUnderflowException e) {  
  9.  int remaining = buf.remaining();  
  10.  buf.get(a, 0, remaining);  
  11.  processBytes(bufArray, remaining);  
  12.  }  
  13.  }  

Conversely, don’t fail silently 

 

 

[java]  view plain copy 在CODE上查看代码片 派生到个人代码片
 
  1. ThreadGroup.enumerate(Thread[] list)  

支持Unchecked Exceptions

 

  • Checked——客户端确定会作一些恢复措施
  • Unchecked——编程错误
  • 过分使用Checked异常会产生一些模板代码

 

[java]  view plain copy 在CODE上查看代码片 派生到个人代码片
 
  1. try {  
  2.  Foo f = (Foo) super.clone();  
  3.  ....  
  4. catch (CloneNotSupportedException e) {  
  5.  // This can't happen, since we’re Cloneable  
  6.  throw new AssertionError();  
  7. }  

异常中应该包含捕获错误的(Failure-Capture)信息

 

 

 

  • 容许诊断和修复或恢复
  • 对于Unchecked异常,有异常消息就好了
  • 对于Checked异常,提供访问器

 

重构API设计

 

 


 

 

 

 

 

 

 

在Vector中进行Sublist操做 

 

[java]  view plain copy 在CODE上查看代码片 派生到个人代码片
 
  1. public class Vector {  
  2.  public int indexOf(Object elem, int index);  
  3.  public int lastIndexOf(Object elem, int index);  
  4.  ...  
  5. }  

分析:

 

 

 

  • 在搜索上不强大
  • 没有文档很难使用

 

 

重构Sublist操做 

 

[java]  view plain copy 在CODE上查看代码片 派生到个人代码片
 
  1. public interface List {  
  2.  List subList(int fromIndex, int toIndex);  
  3.  ...  
  4. }  

分析:

 

 

 

  • 很是强大——支持全部操做
  • 使用接口来减小概念权重:较高的动力重量(power-to-weight)比
  • 没有文档也易于使用

 

线程局部变量 

 

 

[java]  view plain copy 在CODE上查看代码片 派生到个人代码片
 
  1. // Broken - inappropriate use of String as capability.  
  2. // Keys constitute a shared global namespace.  
  3. public class ThreadLocal {  
  4. private ThreadLocal() { } // Non-instantiable  
  5. // Sets current thread’s value for named variable.  
  6. public static void set(String key, Object value);  
  7. // Returns current thread’s value for named variable.  
  8. public static Object get(String key);  
  9. }  

线程局部变量重构1

 

 

 

[java]  view plain copy 在CODE上查看代码片 派生到个人代码片
 
  1. public class ThreadLocal {  
  2. private ThreadLocal() { } // Noninstantiable  
  3. public static class Key { Key() { } }  
  4. // Generates a unique, unforgeable key  
  5. public static Key getKey() { return new Key(); }  
  6. public static void set(Key key, Object value);  
  7. public static Object get(Key key);  
  8. }  

能够运行,可是须要使用模板代码。 

 

 

[java]  view plain copy 在CODE上查看代码片 派生到个人代码片
 
  1. static ThreadLocal.Key serialNumberKey = ThreadLocal.getKey();  
  2.  ThreadLocal.set(serialNumberKey, nextSerialNumber());  
  3.  System.out.println(ThreadLocal.get(serialNumberKey));  

线程局部变量重构2

 

 

 

[java]  view plain copy 在CODE上查看代码片 派生到个人代码片
 
  1. public class ThreadLocal {  
  2.  public ThreadLocal() { }  
  3.  public void set(Object value);  
  4.  public Object get();  
  5.  }  

从API和客户端代码中删除了无用代码。 

 

 

[java]  view plain copy 在CODE上查看代码片 派生到个人代码片
 
  1. static ThreadLocal serialNumber = new ThreadLocal();  
  2.  serialNumber.set(nextSerialNumber());  
  3.  System.out.println(serialNumber.get());  

总结

 

API设计是一件很是高端大气上档次的工艺,对程序员、终端用户和公司都会有所提高。不要盲目地去遵照文中所说起的规则、说明等,但也不要去侵犯他们,API设计不是件简单的工艺,也不是一种能够孤立行动的活。固然完美永远没法实现,但咱们要努力去追求完美。

附上Joshua Bloch的PPT:

来自: How to Design a Good API and Why it Matters

http://www.csdn.net/article/2014-02-18/2818441-How-to-design-a-good-API

相关文章
相关标签/搜索