原链接: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 设计很难
- 不是孤立的活动
 - 完美是无法实现的,但无论如何要努力
 
 
参考: