没有经过设计的代码,是没有灵魂的。
设计涵盖的内容很广,而API设计是与日常开发关系比较紧密的一块。换句话说,掌握了一些核心的设计要领,对代码质量提升是立竿见影的。
无规矩不成方圆,API设计一样。最近学习了Google工程师总结的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
虔诚的写文档
- 为类、接口、方法、参数、异常、构造器编写文档
- 类:实例代表什么
- 方法:方法与使用者之间的约定,如先决条件、后决条件、副作用
- 参数:表示单位、表单、所有权
api设计决策要考虑性能影响
- 不好的决策可能会限制性能,如:
- 类型可变(参考下一条的例子)
- 使用构造方法替换工厂
- 使用实现类型代替接口
- 不要试图通过包装api来提升性能
- 好的设计通常伴随好的性能
api设计决策对性能的影响是真实并且持久的
- Component.getSize() 返回 Dimension
- Dimension是可变的
- 每次调用getSize()将会分配一个Dimension
- 导致数百万不必要的对象
- 即使在1.2版本中增加新方案,老的代码依旧执行很慢
api必须与平台和平共存
- 按规律办事
- 遵守标准的命名约定
- 避免废弃的参数类型和返回类型
- 利用好api的特性
- 泛型、枚举、默认参数、varargs
- 了解并避免api的陷阱
- Finalizers, public static final arrays
类设计
减少可变性
- 类应该是不可变的,除非有更好的理由
- 优点:简单、线程安全、可重用
- 缺点:每个值都有单独的对象
- 如果是可变的,保持状态空间尽可能小和良好的定义
- 明确何时调用那个方法是合法的
Bad: Date, Calendar
Good: TimerTask
子类只出现在有意义的地方
- 子类化意味着可替代性
- 仅当is_a关系才出现子类
- 否则,请使用组合
- 公共类不应该仅仅为了容易实现而子类化其他公共类
Bad: Properties extends Hashtable, Stack extends Vector
Good: Set extends Collection
设计和文档用于继承,否则就禁止它
- 继承违反了封装原则,子类对超类的实现细节敏感
- 如果你允许子类化,文档留作自用
- 保守策略:所有的具体类都final化
Bad: Many concrete classes in J2SE libraries
Good: AbstractSet, AbstractMap
方法设计
不要让使用者做任何模块可以做的事情
- 减少对样板代码的需求
- 通常通过剪切和粘贴来完成
- 丑陋,烦人,容易出错
不要违反最小化原则
- api的用户不应该对api的行为感到惊讶
- 这值得付出很大的努力,甚至牺牲一些性能
public class Thread implements Runnable {
// Tests whether current thread has been interrupted.
// Clears the interrupted status of current thread.
public static boolean interrupted();
}
}
快速失败-当发生错误时,尽可能早的报错
- 编译时最好- 静态类型、泛型
- 运行时,首先调用发生错误的方法
- 方法应该是原子失败的
使用适当的参数和返回类型
- 在输入时,优先选择接口类型而不是类
- _提供灵活性、性能
- 使用最具体的输入参数类型
- 将错误从运行时移动到编译时
- 如果存在更好的类型,不要使用string
- 字符串是笨重的,容易出错的,而且很慢
- 不要使用浮点数来表示货币值
- 二进制浮点导致不精确的结果!
- 使用double(64位)而不是float(32位)
- 精度损失是真实的,性能损失可以忽略不计
使用一致的参数排序
- 尤其重要的是,如果参数类型相同
- 如Collections中的方法,第一个参数一般都是被修改或被查询的。
避免长的参数列表
- 三个或更少的参数是理想的
- 更多的用户将不得不参考文档
- 一长串类型相同的参数是有害的
- _程序员误转了参数
- 程序仍在编译、运行,但行为不正常!
- 两种缩短参数列表的技术
- 拆分方法
- 创建助手类来保存参数
避免需要异常处理的返回值
- 返回零长度数组或空集合,而不是null
Bad:
异常设计
支持unchecked异常
•checked-客户必须采取恢复措施
•unchecked–编程错误
•过度使用checked的异常会导致样板
在异常中包含失败捕获信息
- 允许诊断和修复或恢复
- 对于未检查的异常,消息就足够了
- 对于已检查的异常,提供访问器