原链接:How to Design a Good API and Why it Matters
作者简介:
约书亚·布洛克,曾担任Google的首席Java架构师(Chief Java Architect),《Effective Java》作者,《Java Concurrency In Practice》共同作者,2004年12月,《Java开发者杂志》(Java Developer's Journal)将他列为世界上最顶尖的四十名软件人物之一。
为什么 API 设计如此重要
- API 可以成为公司最大的资产:
- 客户投资巨大:购买、编写、学习
- 停止使用 API 的代价可能会让人望而却步
- 成功的公开API可以抓住用户
- 也可能是公司最大的负债之一
- 糟糕的 API 会导致无休止的电话支持
- 公开 API 永远存在 - 只有一次机会把它写正确
- 只要你开始编程,你就是 API 设计师
- 好的代码是模块化的 - 每个模块都有 API
- 有用的模块往往会被重用
- 模块一旦被使用,就不能随意修改API
- 优秀的可重用模块是公司资产
- 从 API 的角度考虑可以提高代码质量
优秀 API 的特征:
- 易于学习
- 易于使用,即使在没有文档的情况下
- 难以误用
- 易于阅读和维护
- 充分满足需求
- 易于扩展
- 适合使用者的视角(Appropriate to audience)
大纲:
- 一、API 设计过程
- 二、一般原则
- 三、类的设计
- 四、方法设计
- 五、异常设计
- 六、重构 API 设计
一、API 设计过程
1.1 收集需求- 并合理怀疑
- 通常你会得到建议的解决方案
- 可能存在更好的解决方案
- 你的工作是提炼真实的需求
- 应该使用 Use-Cases(这样做非常好)
- 更通用的逻辑可以让工作更轻松和更有意义
1.2 从简短清单开始 - 1页就够
- 这个阶段,敏捷胜过完整
- 让尽可能多的人知道
- 听取他们的意见并认真对待
- 如果保持设计文档非常简短,修改就非常容易
- 当你有信心时,补充更多细节
- 这必然涉及编码
1.3 尽早编写 API
- 在实现前开始编写 API
- 节省那些会被你抛弃的实现
- 在正式明确前开始
- 节省会被你抛弃的 API 描述
- 补充细节后持续编写
- 避免意外
- 代码作为示例、单元测试而存在
1.4 编写 SPI 更重要
- Service Provider Interface(SPI)
- 插件接口支持多种实现
- 示例:Java Cryptography Extension (JCE)
- 在发布之前编写多个插件
- 如果你写一个,它很有可能不支持其他的(不好的实现)
- 如果你写两个,它将艰难的支持更多
- 如果你写三个,就能很好的工作(这样做非常好)
- 威尔·贡培兹称之为“三的规则”(《二手程序销售员的自白》,培生教育出版集团,1995)
1.5 保持现实的期望(Maintain Realistic Expectations,回到现实世界?)
- 大多数API设计都是过约束的
- 你不可能取悦所有人
- 结果可能是让每个人都不高兴(Aim to displease everyone equally?)
- 期望会犯错误(Expect to make mistakes)
- 实际使用不久就会被淘汰
- 期望改进 API
二、一般原则
2.1 API 应该只做一件事,且把它做好
- 功能应该易于解释
- 如果命名很困难,这通常是个坏兆头
- 好的名字可以驱动开发
- 易于拆分和合并模块
2.2 API 应该尽可能小,且不能再小
- API 应该满足它的需求
- 当 API 不可靠时,就废弃(When in doubt leave it out)
- 功能、类、方法、参数,等等
- 你可以添加,但永远不能删除
- 概念权重比主体重要
- 找到高性价比的实现(Look for a good power-to-weight ratio)?
2.3 实现不应影响 API
- 实现细节
- 混淆用户
- 禁止随便改变实现
- 了解实现细节是什么
- 不要过度指定方法的行为,比如 不要制定 哈希函数
- 所有的调整参数都是可疑的
- 不要让实现细节泄露到 API
- 磁盘和在线格式,例外(On-disk and on-the-wire formats, exceptions?)
2.4 最小化一切的可访问性
- 让类、成员变量尽可能私有
- 公开的类不要有公开的类成员(常量除外)
- 最大程度信息隐藏(系统的局部设计简化)
- 允许模块独立使用,被理解,构建,测试和调试
2.5 命名的重要性 - API就是一种微型语言
- 名称应该最大程度的自解释
- 避免含糊的缩写
- 保持一致—— 同一词表示同一事物
- 平台上的所有 API
- 有规律的——力求相似
- 代码读起来应该像散文
if (car.speed() > 2 * SPEED_LIMIT)
generateAlert("Watch out for cops!");
2.6 文档的重要性
复用性说起来容易做起来难,做好它不仅需要良好的设计,还需要良好的文档。即使我们看到好的设计(这种情况仍然很少见),但如果没有好的文档,我们也不会看到组件的重用。
- D. L. Parnas,_软件老化。 第十六届国际软件工程会议,1994年
2.7 审慎认真的记录
- 记录每一个类、接口、方法、构造函数、参数和异常
- Class:实例作用是什么
- 方法:方法和使用者间的合约
- 先决条件、后置条件、副作用
- 参数:标明单元(units)、组成(form)、所有权
- 认真记录作用域(Document state space very carefully)
2.8 考虑API设计决策的性能后果
- 错误的决策会限制性能
- 使类型可变
- 提供构造函数而不是静态工厂
- 使用实现类型而不是接口
- 不要扭曲 API 以获得性能
- 基本的性能问题将得到解决,但头痛将永远伴随着你
- 好的设计通常与好的性能相一致
- API 设计决策对性能的影响是真实和永久的
- Component.getSize() 返回 Dimension
- Dimension 是可变的
- 每一次 getSize 调用都需要分配 Dimension 内存
- 导致成千上万的不必要的对象内存分配
- 在 1.2 中增加了替代方案;不过旧的代码仍然很慢
2.9 API 必须与平台和平共处
- 遵守惯例
- 遵守标准命名约定
- 避免废弃(过时)的参数和返回类型
- 模拟核心 API 和编程语言的模式
- 利用 API 友好的特性
- 泛型、变量参数、枚举、默认参数
- 了解并避免 API 缺陷与陷阱
- Finalizers、public static final arrays(?)
三、类的设计
3.1 最小化易变性
- 除非有充分的理由,否则类应该是不可变的
- 优点:简单,线程安全,可重用
- 缺点:为每个值创建对象
- 如果易变,保证作用域足够小,定义明确
- 明确什么时机调用哪个方法是合法的
坏的示例:Date,Calendar
好的示例:TimerTask
3.2 仅在有意义的时候使用子类
- 子类化意味着可替代性(里氏替换,Liskov)
- 仅当 is-a 的关系存在时才使用子类
- 否则,使用组合
- 为了便于实现,公共类不应该子类化其他公共类
坏的示例:Properties extends Hashtable, Stack extends Vector
好的示例:Set extends Collection
Collection是接口,Hashtable 和 Vector是具体的实现。
3.3 为继承设计并文档记录它,否则禁止使用继承
- 继承破坏封装
- 子类对父类的实现细节敏感
- 如果允许子类化,文档标明 作用
- 方法如何互相调用
- 保守政策:所有实体类都是final
坏的示例:Many concrete classes in J2SE libraries
好的示例:AbstractSet, AbstractMap
四、方法设计
4.1 不要让客户端做模块能做的任何事情
- 减少对样板代码的需求
- 通常通过剪切-粘贴完成
- 丑陋,烦人,容易出错

4.2 不要违反最小惊奇原则
- API 的使用者不应为 API 的行为感到惊讶
- 值得付出的额外努力
- 甚至值得降低性能

4.3 Fail Fast–错误发生后尽快报告
- 最好是在编译时- 静态类型、泛型
- 运行时,最好在第一个错误的方法调用是时
- 方法的失败应该是原子的

4.4 对所有可用数据提供字符串形式的编程访问
- 否则,客户端需要转换为字符串
- 这很痛苦
- 更坏的是,将字符串格式转化为真实的API

4.5 重载要小心
- 避免不明确的重载
- 多个重载方法实际结果一致
- 保守:没有两个重载方法的参数数量相同
- 仅仅因为你可以,并不意味着你应该
- 通常更好的办法是使用不同的名字
- 如果你一定要提供不明确的重载方法,确保他们对同样的参数有同样的行为

4.6 使用恰当的参数和返回数据类型
- 优先选择接口而不是类作为输入
- 能保证灵活性和性能
- 在可能输入参数类型中,使用最具体的数据类型
- 将错误从运行时前置到编译时
- 如果有更好的选择类型,就不要使用字符串
- 字符串繁琐,容易出错且运行缓慢
- 不要对货币数值使用浮点类型
- 二进制浮点会导致结果不够精确
- 使用 double(64 bits)而不是float类型(32 bits)
- 精度损失为实,性能损失可忽略不计
4.7 使用一致性的参数顺序
- 如果参数类型相同,则尤为重要

4.8 避免长参数列表
- 三个或更少的参数是理想的
- 更多参数用户将不得不参考文档
- 参数类型相同的长参数列表有害
- 程序员很容易将参数顺序写反而犯错
- 程序仍然可以编译,运行,但是行为异常
- 两种缩短参数列表的方法:
- 拆分方法
- 创建 Helper 类以保存参数

4.9 避免异常返回值处理
- 返回元素数为 0 的数组或集合类,而不是 null

五、异常设计
5.1 抛出异常标明异常状况
- 不要强制客户端对控制流使用异常

- 相应的,也不要悄悄的失败

5.2 支持未经检查的异常
- 已检查 - 客户端必须执行恢复操作
- 未检查 - 程序错误
- 过度使用异常检查产生样板代码

5.3 在异常中包含故障捕获信息
- 允许诊断,修复或恢复
- 对于未经检查的异常,消息就足够了
- 对于已检查的异常,提供访问器
六、重构 API 设计
6.1 Vector中的子列表操作
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); ...
}
- 极其强大-支持所有操作
- 使用接口可减轻概念上的负担
- 高性价比
- 无需文档,易于使用
6.2 线程局部变量(Thread-Local Variables)
// Broken - inappropriate use of String as capability.
// Keys constitute 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);
}
线程局部变量重构(1)
public class ThreadLocal {
private ThreadLocal() { } // Noninstantiable
public static class Key { Key() { } }
// Generates a unique, unforgeable key
public static Key getKey() { return new 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));
线程局部变量重构(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 设计很难
- 不是孤立的活动
- 完美是无法实现的,但无论如何要努力
参考: