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

相關文章
相關標籤/搜索