本文翻译自 Effective Java 作者 Joshua Bloch 撰写的一篇关于 API 设计的分享

API 可以是公司最大的资产之一

  • 客户投入巨资:购买、撰写、学习(售前一系列培训)
  • 停止使用 API 导致的花费令人望而却步(如果不使用 API 可能要花费更多)
  • 成功的公共 API 赢得客户

也可以成为公司最大的负债之一

  • 糟糕的 APIs 将导致无休止的电话技术支持(需要接听很多来自客户的反映)

公共的 APIs 是永久的——一次去做正确事情的机会

为什么 API 设计对于你来说很重要

如果你编码,你就是一个 API 的设计者

  • 良好的代码应该是模块化的——每一个模块都有一个 API
  • 有用的模块往往被重用

对于 API 方面的思考将提高代码的质量

好的 API 所具备的特征

  • 易于学习
  • 易于使用,甚至无需任何文档
  • 不易误用
  • 易于阅读并且对所使用的代码部分易于维护
  • 足够强大以满足需求
  • 易于拓展
  • 适用于大众

大纲

  • API 设计的过程
  • 一般原则
  • 类设计
  • 方法设计
  • 异常设计
  • API 重构设计

I API 设计的过程

收集需求——以一种健康的程度怀疑

通常你会得到建议的解决方法作为替代方案

  • 可能存在更好的解决方案

你的工作是去提取出真正的需求

  • 应采取用例的形式
  • 可以更容易、更有意义地建立更普遍的东西

从简短的规范页开始比较理想

  • 在这个阶段,敏捷胜过完整性
  • 和尽可能多的人谈(原文:Bounce spec off as many people as possible) 倾听他们的输入并认真对待
  • 如果你保持规范简短,那么将易于修改
  • 充分自信 这涉及到编码时也很必要

尽早写给你的 API

以下应开始于在你实现 API 之前

  • 从你将扔掉的实现中拯救你

以下应开始于你正确指定出 API 之前

  • 从撰写你将扔掉的规范中拯救你

继续写 API 因为它充实你

  • 避免令人讨厌的惊喜
  • 代码作为例子、单元测试而存在

关于 SPI 的内容甚至更重要

服务提供接口(Service Provider Interface, SPI)

  • 插件式接口使得实现多样性
  • 例如:Java 加密拓展接口(Java Cryptography Extension, JCE)

在发布之前编写多个插件

  • 如果你只写一个,它可能不支持另一种情况
  • 如果你写两个,它会很难支持更多
  • 如果你写三个(原文 three 应表示多个?),它会良好工作

Will Tracz 称之为“三项法则”

(曾经以为程序销售员 Addision-Wesley,1995 的自白)

保持现实的期望

大多数 API 设计的过约束

  • 你不必取悦每一个人
  • 旨在平等地使每个人都感到高兴

期望犯错

  • 几年真实世界的使用将冲洗它们
  • 期望发展 API

II 一般原则

API 应该只做一件事并把它做好

功能应该易于解释

  • 如果很难命名 API,那它通常是一个坏讯号
  • 良好的命名会驱动开发
  • 要易于分割和合并

API 应尽可能的小但不能再小

API 应该满足它的初始需求

当存疑时就抛弃掉

  • 函数、类、方法、参数等等
  • 你总可以加些什么,但你永远不能去掉什么

概念的重量比实体块的重量更重要

寻找一个好的力量/重量比率(此处应指 API 的作用和轻重之比)

实现不应该影响 API

实现细节

  • 迷惑用户
  • 禁止掉改变 API 实现的自由

意识到实现细节是什么

  • 不要过度指定方法的行为
  • 例如:不要指定散列函数
  • 所有的可调整参数都是可疑的

别让实现细节“泄露”进 API

  • 磁盘上的格式和线上的格式例外

最小化对于所有的可达性

将类和方法指定得尽可能私有

公共类不应该有公共域(除了常量)

这最大化了信息隐藏

允许模块被独立使用、理解、构建、测试、调试

给 API 命名的事务相当于一种小语言

名字应大部分不言自明(自解释的)

  • 避免模糊的缩写

保持一致——同样的词应表达同样的意思

  • 贯穿 API 的整个内容(包括不同平台上的该 API)

定期争取对称

代码应该读起来像散文

if (car.speed() > 2 * SPEED_LIMIT)
    generateAlert("Watch out for cops!");

文档相关事宜

重用是一件说起来容易做起来难的事。具体做起来既需要好的设计又需要良好的文档。即使我们看到好的设计,我们仍很少能看到没有好的文档组件被重用。

D.L.Parnas, _Software Aging. Proceedings of 16th International Conference Software Engineering, 1994

Document Religiously

为每一个类、接口、方法、构造器、参数和异常制作文档(注释)

  • 类:实例所表示的东西
  • 方法:方法和客户之间的契约
    • 先决条件、后置条件、副作用
  • 参数:提示性的单位、格式、所有权

文档要非常认真地陈述

考虑 API 设计决定的性能后果

糟糕的决定会限制性能

  • 让类型易变
  • 提供构造器以取代静态工厂
  • 使用实现类型取代接口

不要扭曲 API 来获得性能

  • 底层性能问题会被修复,但头疼的事会一直伴随着你
  • 良好的设计通常与良好的性能相吻合

API 设计决策在性能方面的影响是真实并且永久的

  • Component.getSize() returns Dimension
  • 尺寸是易变的
  • 每一个 getSize 调用都必然分配 Dimension
  • 将导致非常多不必要的对象分配
  • 在 1.2 版本增加替代品,老的客户端代码仍然慢(在新的版本找到解决方案,但不能解决旧代码的性能问题)

API 必须和平台和平共存

做习惯性的事

  • 遵守标准的命名约定
  • 避免过时的参数和返回类型
  • 模仿核心 APIs 和语言中的模式

利用 API 友好功能

  • 泛型、可变参数、枚举、默认参数

了解并避免 API 陷阱

  • 常量(Finalizers)、公共静态常量数组

III 类设计

最小化可变性

类应该是不可变的除非有一个好的理由不去这么做

  • 优点: 简洁、线程安全、可复用
  • 缺点: 对于每个值都分离了对象

如果可变,保证状态空间尽可能小并被良好定义

  • 搞清何时去调用哪一个方法是合法的

仅在合理的地方建子类

建子类按时可替代性(Liskov)

  • 子类仅当 is-a 关系时存在
  • 否则,使用组合

公共类不应该再包含其他公共子类,以保证实现简单

反例: Properties extends Hashtable Stack extends Vector

正例: Set extends Collection

为继承做设计和文档否则禁止

继承违反封装(Snyder, 86)

  • 子类对于父类的实现细节敏感

如果你允许建子类,那么就文档自用

  • 方法如何相互使用

保守策略:所有具体的类都不可变(final)

反例:J2SE 包中许多的具体类 正例:AbstractSet, AbstractMap

IV 方法设计

别让客户端把模板能做的事都做了

减少对样板代码的需要

  • 通常经 cut-and-paste 完成
  • 丑陋、恼人并且易错

不要违反最小原则

API 的使用者不应该对于某些行为感到惊讶

  • 值得额外的实现努力
  • 这甚至值得降低性能
public class Thread implements Runnable {
    // Tests whether current thread has been interrupted
    // Clears the interrupted status of current thread.
    public static boolean interrupted();
}

上面的第二行注释所述的功能就做了额外的没必要的努力,违背了最小原则

当错误发生尽可能快地产生错误报告

编译时报错最佳——静态拼写、generics

在运行时,第一个方法调用失败为最佳

  • 方法应该是原子性失败(failure-atomic)
// A Properties instance maps strings to strings
public class Properties extends Hashtable {
    public Object put(Object key, Object value);

    // Throws ClassCastException if this Properties
    // contains any keys or values that are not strings
    public void save(OutputStream out, String comments);
}

为所有的数据访问提供字符串的形式的编程接口

否则客户端需要做字符串转换

  • 对于客户端来说很痛苦
  • 更糟糕的是,turns strings format into de facto API(无力翻译 orz)
public class Throwable {
    public void printStackTrace(PrintStream s);
    public StackTraceElement[] getStackTrace();		// Since 1.4
}

public final class StackTraceElement {
    public String getFilaName();
    public int getLineNumber();
    public String getClassName();
    public String getMethodName();
    public boolean isNativeMethod();
}

小心重载

避免模糊的重载

  • 多个重载适用于相同的情况
  • 保守策略:没有两个重载拥有同样多的参数个数

仅仅是因为你可以但不意味着你应当

  • 通常最好使用不同的名字

如果你必须提供模糊的重载,请确保相同的参数拥有相同的行为

反例:

public TreeSet(Collection c);		// Ignores order
public TreeSet(SortedSet s);		// Respects order

使用恰当的参数和返回类型

赞成接口类型作为类的输入

  • 提供灵活性、性能

使用最具体的可能的输入参数类型

  • 把运行时错误提前到编译时

如果存在更好的类型请别用 String 类型

  • Strings 是繁琐的、易错的并且慢的

不要将浮点数用于货币值

  • 二进制浮点会导致不精确的结果

使用 double(64 位)优于 float(32 位)

  • 精确度损失是真实的,性能损失是可忽略的

在方法之间使用一致的参数顺序

如果参数类型相同,尤其重要

反例:

#include <string.h>
char *strcpy (char *dest, char *src);
void bcopy   (void *src, void *dst, int n);

正例: java.util.Collections——第一个参数总被收集来用于修改或查询

java.util.concurrent——time 总被指定为 long delay, TimeUnit unit

避免长参数列表

三个或更少参数是比较理想的

  • 如果存在更多,用户则需参阅文档

相同类型的长参数列表是有害的

  • 程序员会错误地转置参数
  • 程序仍会编译、运行,但行为不端

两种缩短参数列表的技巧

  • 拆分方法
  • 创建辅助类来保存参数

反例:

// Eleven parameters including four consecutive ints
HWND CreateWindow(LPCTSTR lpClassName, LPCTSTR lpWindowName,
      DWORD dwStyle, int x, int y, int nWidth, int nHeight,
      HWND hWndParent, HMENU hMenu, HINSTANCE hInstance,
      LPVOID lpParam);

避免返回值需要异常处理

返回零长度数组或空集合而非 null

package java.awt.image;
public interface BufferedImageOp {
    // Returns the rendering hints for this operation,
    // or null if no hints have been set.
    public RenderingHints getRenderingHints();
}

V.异常设计

抛出异常以表明异常的条件

不要强迫客户端去使用异常来控制流

反例:

private byte[] a = new byte[BUF_SIZE];
void processBuffer (ByteBuffer buf) {
  try {
    while (true) {
      buf.get(a);
      processBytes(tmp, BUF_SIZE);
    }
  } catch (BufferUnderflowException e) {
    int remaining = buf.remaining();
    buf.get(a, 0, remaning);
    processBytes(bufArray, remaining);
  }
}

反过来,不要安静的失败

例如:

ThreadGroup.enumerate(Thread[] list)

赞成未经检查的异常

已检查——客户端必须采取修复措施

未检查——程序报错

过度使用已检查的异常会导致样板化

反例:

try {
    Foo f = (Foo) super.clone();
    ...
} catch (CloneNotSupportedException e) {
    // This can't happen, since we're Cloneable
    throw new AssertionError();
}

在异常中包含错误捕获信息

允许诊断、修复或恢复

对于未检查的异常,信息就足够了

对于已检查的异常,提供访问者

VI 重构 API 设计

向量的子列表操作

public class Vector {
    public int indexOf(Object elem, int index);
    public int lastIndexOf(Object elem, int index);
}

不够强大——只支持搜索

没有文档很难使用

字列表操作重构

public interface List {
    List subList(int fromIndex, int toIndex);
    ...
}

非常强大——支持所有的操作

接口的使用减少了概念的重量

  • 高功率重量比

没有文档也易于使用

Thread-local 变量

// Broken - inappropriate use of String as capability.
// Keys constitue a shared global namespace.
public class ThreadLocal {
    private ThreadLocal() { }    // Non-instantiable

    // Sets current thread's value for named variable.
    public static void set(String key, Object value);

    // Returns current thread's value for named variable.
    public static Object get(String key);
}

Thread-Local 变量重构(1)

public class ThreadLocal {
    private ThreadLocal() { }  // Noninstantiable

    public static class Key {	Key() { } }
    // Generates a unique, unforgeable key
    public static void set(Key key, Object value);
    public static Object get(Key key);
}

有效,但是需要使用样板代码

例如:

static ThreadLocal.Key serialNumberKey = ThreadLocal.getKey();
ThreadLocal.set(serialNumberKey, nextSerialNumber());
System.out.println(ThreadLocal.get(serialNumberKey));

Thread-Local 变量重构(2)

public class ThreadLocal {
    public ThreadLocal() { }
    public void set(Object value);
    public Object get();
}

消除 API 和客户端代码之间的混乱

static ThreadLocal serialNumber = new ThreadLocal();
serialNumber.set(nextSerialNumber());
System.out.println(serialNumber.get());

结论

API 设计是一种高贵且有益的工艺

  • 改进了很多程序员、最终用户和公司

这次谈话涵盖了一些启发式的手艺

  • 不要狂妄地坚持,但…
  • 不要没有理由的违反

API 设计很难

  • 不是一个孤独的活动
  • 完美是无法实现的,但无论如何都要尝试

个人评价&总结

本文评价了很多 Java 语言包以及面向对象的优点和缺点,也包含 C 语言的部分例子。本文作者 Joshua Bloch 同时也是 Effective Java 的作者。本文中有些用词比较晦涩难懂,但通篇整体很好也很全面地剖析了 API 设计的重要性及一部分技巧和注意事项,值得参考借鉴。

翻译过程也是磕磕绊绊,很多地方自我感觉翻译的不甚准确,甚至有问题,实在找不到合适的翻译的地方注明了原文或保留原文词句,希望有心读者能指正并反馈。不过翻译的过程的确为了理解作者原意,也会竭尽脑力去思考和体会字里行间所表达的思想,收益良多。 其中不乏有些建议的确出现于日常的编程实践,并作为编程规范存在于最佳实践中,一眼就能理解。也有一些目前可能尚未在实际应用中良好实践。

原文链接:https://web.archive.org/web/20110903030015/http://lcsd05.cs.tamu.edu/slides/keynote.pdf