在设计Java API的时候总是有很多不同的规范和考量。与任何复杂的事物一样,这项工作往往就是在考验我们思考的缜密程度。就像飞行员起飞前的检查清单,这张清单将帮助软件设计者在设计Java API的过程中回忆起那些明确的或者不明确的规范。本文也可以看作为“API设计指南”这篇文章的附录。
我们还准备了一些前后比对的例子来展示这个列表如何帮助你理清设计需求,找出错误,识别糟糕的设计实践以及如何寻找改进的时机。
这个清单使用了如下的语言规范:
要 – 表示必要的设计
建议 – 表示在几个最好的设计中选择一个
考虑 – 表示一个可能的设计上的改进
避免 – 表示一个设计上的缺陷
不要 – 表示一个设计上的错误
1. 包设计清单
1.1. 共通
▲1.1.1. 建议把API和实现放入不同的包
▲1.1.2. 建议把API放进上层包,而把实现放进下层包
▲1.1.3. strong>考虑把一组大型的API分拆进不同的包
▲1.1.4. 考虑把API和实现打包进不同的jar包
▲1.1.5. 避免API的实现类之间的内部依赖
▲1.1.6. 避免把API分拆了太细
▲1.1.7. 避免把公共实现类放到API的包中
▲1.1.8. 不要在调用者和实现类之间建立依赖
▲1.1.9. 不要把没有关系的API放进同一个包
▲1.1.10. 不要把API和SPI(service provider interface)放进一个包(注:两者不同可以查看这个页面http://stackoverflow.com/questions/2954372/difference-between-spi-and-api)
▲1.1.11. 不要移动或者重命名一个已经发布的公共API
1.2. 命名
▲1.2.1. (一级)包名以公司(或者组织)的根命名空间来命名
▲1.2.2. 使用一个稳定的产品名称或者一个产品系列的名称作为包的二级名称
▲1.2.3. 使用API名称作为包名的(三级名称)结尾
▲1.2.4. 考虑把仅包含实现的包的名称中包含”internal”这个词(注:似乎“impl”更常见一些)
▲1.2.5. 避免使用组合起来的名称
▲1.2.6. 避免包名和包内的类名使用同样的名称
▲1.2.7. 避免在包名称中使用“api”这个词
▲1.2.8. 不要使用营销,计划,组织单元(部门)和地理名称
▲1.2.9. 不要在包名中使用大写字母
1.3. 文档
▲1.3.1. 为每一个包提供一个package.html
▲1.3.2. 遵循标准的javadoc的规范
▲1.3.3. 在API的开始处用一句短小的话来概括(描述)
▲1.3.4. 提供足够多的细节来帮助判断是否需要使用和如何使用该API
▲1.3.5. 指出该API的入口(主要的类或者方法)
▲1.3.6. 包含覆盖主要的,基本功能演示的样例代码
▲1.3.7. 包含一个指向开发者指南的超链接
▲1.3.8. 包含一个指向手册的超链接
▲1.3.9. 指出相关的API集合
▲1.3.10. 包含API的版本号
▲1.3.11. 用@deprecated标记出不再使用的API版本
▲1.3.12. 考虑添加一个版权声明
▲1.3.13. 避免过长的包概述
▲1.3.14. 不要在发布的javadoc中包含实现相关的包
2. 类型设计清单(这里的“类型”个人理解为一组Api)
2.1. 共通
▲2.1.1. 确保每种(设计的)类型都有单一明确的目的
▲2.1.2. 确保每种类型代表了(业务)领域的概念,而不是为了(技术上)的抽象
▲2.1.3. 限制类型的总数量
▲2.1.4. 限制类型的大小
▲2.1.5. 设计相关的类型时保持和原有的类型的一致性
▲2.1.6. 建议为多种public的类型提供多种(private)的实现
▲2.1.7. 建议接口的实现类和继承关系的类应该在行为上保持一致性
▲2.1.8. 建议用抽象类而不是接口解耦Api的实现
▲2.1.9. 建议使用枚举而不是常量
▲2.1.10. 考虑使用泛型
▲2.1.11. 考虑在泛型参数上增加约束
▲2.1.12. 考虑使用接口来实现多继承的效果
▲2.1.13. 避免为使用者的扩展(需求)进行设计
▲2.1.14. 避免深度的继承层次
▲2.1.15. 不要使用public内嵌的类型
▲2.1.16. 不要申明public和protected的变量
▲2.1.17. 不要把实现的继承关系暴露给使用者
2.2. 命名
▲2.2.1. 使用名词或者名词词组
▲2.2.2. 使用PascalCasing(驼峰命名法的别称详见http://en.wikipedia.org/wiki/CamelCase)
▲2.2.3. 缩写仅第一个首字母大写
▲2.2.4. 为类型的实际作用使用精确的名称命名
▲2.2.5. 为最常用的类型准备最短最容易记忆的名称
▲2.2.6. 所有异常都以“Exception”结尾
▲2.2.7. 使用名词的单数(比如用Color而不用Colors)来命名枚举类型
▲2.2.8. 考虑较长的名称
▲2.2.9. 考虑派生类用基类的名称结尾
▲2.2.10. 考虑为抽象类名称使用“Abstract”开头
▲2.2.11. 避免使用缩略语
▲2.2.12. 避免太通用的名词
▲2.2.13. 避免同义词
▲2.2.14. 避免在相关的Api中使用类型的名称
▲2.2.15. 不要使用仅大小写不同的名称
▲2.2.16. 不要使用前缀
▲2.2.17. 不要以“I”作为接口的名称开头
▲2.2.18. 不要(重复)使用Java核心包中的名称
2.3. 类
▲2.3.1. 最小化实现使用的依赖
▲2.3.2. 先列出public方法
▲2.3.3. 申明实现方法为private(这里是笔误吗?)
▲2.3.4. 为一个public抽象类定义至少一个public的shi
▲2.3.5. 为基本的使用情况提供足够的缺省实现
▲2.3.6. 设计基本上不变的类
▲2.3.7. 把无状态,访问器,扩展(mutator个人理解为多种参数形式的方法)方法集合到一起
▲2.3.8. 把扩展方法的数量控制到最少
▲2.3.9. 考虑设计一个默认的无参的构造方法
▲2.3.10. 考虑重写equal,hashCode方法
▲2.3.11. 考虑实现Comparable接口
▲2.3.12. 考虑实现Serializable接口
▲2.3.13. 考虑使类可以容易的扩展
▲2.3.14. 考虑申明类为final
▲2.3.15. 考虑为类的实例化提供一个public的构造方法
▲2.3.16. 考虑使用自定义的类型来增强类的不可变性
▲2.3.17. 考虑设计不可变的类
▲2.3.18. 避免静态类
▲2.3.19. 避免使用Cloneable
▲2.3.20. 不要向静态类中添加实例duixi
▲2.3.21. 不要为使用者不应扩展的public抽象类提供public的构造方法
▲2.3.22. 不要滥用初始化
2.4. 接口
▲2.4.1. 为每一个public接口提供至少一个实现类
▲2.4.2. 为每一个public接口设计至少一个消费方法
▲2.4.3. 不要对一个已经发布的public接口添加新的方法
▲2.4.4. 不要使用标记接口(标记接口详见http://en.wikipedia.org/wiki/Marker_interface_pattern)
▲2.4.5. 不要把public接口设计成常量的容器(这个实在很常见啊……)
2.5. 枚举
▲2.5.1. 考虑为枚举类型指定一个0值(“NONE”或者“Unspecialized”等等)
▲2.5.2. 避免只有一个值的枚举
▲2.5.3. 不要使用枚举实现开放式的值集合
▲2.5.4. 不要为将来可能增加的值设计枚举
▲2.5.5. 不要为已经发布的版本增加新的枚举值
2.6. 异常
▲2.6.1. 确保自定义的异常可以被序列化
▲2.6.2. 考虑为每种类型定义一个不同的异常
▲2.6.3. 考虑为代码访问提供更多的异常信息
▲2.6.4. 避免深层的异常继承
▲2.6.5. 不要从Exception和RuntimeException以外的类派生自定义异常
▲2.6.6. 不要直接从Throwable派生异常
▲2.6.7. 不要在异常信息内包含敏感信息
2.7. 文档
▲2.7.1. 为每种类型(的Api)配上概述
▲2.7.2. 遵循标准Javadoc的约定
▲2.7.3. 每种类型开头以一句短小的话概述
▲2.7.4. 为是否使用以及如何使用该类型提供足够的细节来帮助做决定
▲2.7.5. 解释如何实例化一个类型
▲2.7.6. 为一个类型的主要的使用情景提供样例代码
▲2.7.7. 包含指向到开发指南的链接
▲2.7.8. 包含指向手册的链接
▲2.7.9. 显示相关的类型
▲2.7.10. 用@deprecated标签申明过时的类型
▲2.7.11. 文档类具有不可变性
▲2.7.12. 避免冗长的类概述
▲2.7.13. 不要为私有方法生成Javadoc
3. 方法设计清单
3.1. 共通
▲3.1.1. 确保每个方法实现一个目的
▲3.1.2. 确保相关的方法都是一个粒度级别的
▲3.1.3. 确保没有混合调用方法的公共代码
▲3.1.4. 使所有方法的调用具有原子性(原子性:http://jiangyongyuan.iteye.com/blog/364010)
▲3.1.5. 设计protected方法时要像public方法一样慎重
▲3.1.6. 限制扩展方法的数量
▲3.1.7. 设计扩展方法需要具有较强的稳定性
▲3.1.8. 建议为一系列重载的方法设计一个泛型的方法
▲3.1.9. 考虑使用泛型方法
▲3.1.10. 考虑设计方法对,即两个方法的作用是相反的
▲3.1.11. 避免“helper”方法
▲3.1.12. 避免长时间执行的方法
▲3.1.13. 避免调用者在普通使用中需要手动写循环
▲3.1.14. 避免可选的参数影响方法的行为
▲3.1.15. 避免不可重复调用的方法
▲3.1.16. 不要删除一个已经发布的方法
▲3.1.17. 不要在没有提供替换方法前把一个已经发布的方法标记为过时
▲3.1.18. 不要修改一个已经发布的方法的签名
▲3.1.19. 不要修改一个已经发布的方法的可观测行为(也许指的是输出之类)
▲3.1.20. 不要增加一个已经发布方法的调用条件
▲3.1.21. 不要减少一个已经发布方法的调用结果
▲3.1.22. 不要为已经发布的public接口新增方法
▲3.1.23. 不要为已经发布的Api新增重载
3.2. 命名
▲3.2.1. 用给力的,有表达力的动词作为名称起始
▲3.2.2. 使用驼峰命名法(好奇怪,前面写的是PascalNaming)
▲3.2.3. 为JavaBean的私有属性预留“get”“set”“is”等访问方法
▲3.2.4. 使用对调用者熟悉的词语
▲3.2.5. 尽量使用英语口语
▲3.2.6. 避免使用缩略语
▲3.2.7. 避免使用一般的动词
▲3.2.8. 避免同义词
▲3.2.9. 不要使用“黑话”
▲3.2.10. 不要依靠参数的名称和类型判断方法的意义
3.3. 参数
▲3.3.1. 为参数选择最合适的类型
▲3.3.2. 在相关方法的调用中对参数为null值的处理保持一致性
▲3.3.3. 在相关方法中参数的名称,类型和顺序需要保持一致
▲3.3.4. 在参数列表中把输出的参数放到输入参数之后
▲3.3.5. 为重载的方法省略常用的默认参数以提供一个较短的参数列表
▲3.3.6. 在无关的类型中为相同语义的操作提供重载方法
▲3.3.7. 建议使用接口而不是具体类作为参数
▲3.3.8. 建议使用集合而不是数组作为参数和返回值
▲3.3.9. 建议使用一般集合而不是原始(无类型)集合
▲3.3.10. 建议使用枚举而不是Boolean或者Integer作为参数
▲3.3.11. 建议把单个的参数放到集合或者数组参数之前
▲3.3.12. 建议把自定义类型的参数放大Java标准类型参数之前
▲3.3.13. 建议把对象类型的参数方法值类型的参数之前
▲3.3.14. 建议使用接口而不是具体类作为返回值
▲3.3.15. 建议把空的集合而不是null作为返回值
▲3.3.16. 建议把返回值设计成可以作为其他方法的合法输入参数
▲3.3.17. 考虑为不可变参数设计一个副本
▲3.3.18. 考虑在内部存储弱引用的对象
▲3.3.19. 避免参数数量变更
▲3.3.20. 避免参数长度太长(超过3个)
▲3.3.21. 避免连续的同类型的参数
▲3.3.22. 避免用作输出或者输入输出的参数
▲3.3.23. 避免方法重载
▲3.3.24. 避免参数类型暴露实现细节
▲3.3.25. 避免boolean参数
▲3.3.26. 避免返回null
▲3.3.27. 除了Java核心Api,避免把类型作为不相关的Api的返回值
▲3.3.28. 避免把可变的内部对象作为返回值来引用
▲3.3.29. 不要把预先设置的常量作为整型值参数使用
▲3.3.30. 不要为将来的(扩展设计)考虑预留参数
▲3.3.31. 不要在重载方法中改变参数的名称的顺序
3.4. 异常处理
▲3.4.1. 只有在异常情况下才抛出异常
▲3.4.2. 只需要为可恢复的错误抛出已确认的异常
▲3.4.3. 为了通知Api使用错误而抛出运行时异常
▲3.4.4. 在适当的抽象层次抛出异常
▲3.4.5. 进行运行时预置条件的检查
▲3.4.6. 为一个被不能为null的参数抛出空指针异常
▲3.4.7. 为一个除为null以外异常值的参数排除非法参数异常
▲3.4.8. 为一个错误上下文环境中的方法调用抛出非法状态异常
▲3.4.9. 在错误信息中显示出参数的预置条件
▲3.4.10. 确保失败的方法调用不会产生单向的后果
▲3.4.11. 为回调方法中的禁止使用的Api提供运行时检查
▲3.4.12. 建议优先使用Java标准异常
▲3.4.13. 建议提供抛出异常的条件的查询方法
3.5. 重写
▲3.5.1. 使用@Override注解
▲3.5.2. 维持或弱化预置条件
▲3.5.3. 维持或者加强后置条件(不好翻译,大概output+effect的意思)
▲3.5.4. 维持或者加强不可变性
▲3.5.5. 不要抛出新增的运行时异常
▲3.5.6. 不要更改方法的类型(无状态,访问器或者扩展方法等)
3.6. 构造方法
▲3.6.1. 最小化构造方法中的工作
▲3.6.2. 为所有的属性设置合理的默认值
▲3.6.3. 仅把构造方法的参数作为一种设置参数的快捷方法
▲3.6.4. 校验构造方法的参数
▲3.6.5. 以参数相应的属性为其命名
▲3.6.6. 当提供了多个构造方法时,遵循指南对其进行重载
▲3.6.7. 建议使用构造方法而不是静态的工厂方法
▲3.6.8. 考虑使用无参的构造方法
▲3.6.9. 如果不是总需要新的实例,考虑使用静态的工厂方法
▲3.6.10. 如果你需要在运行时决定一个合适的类型,考虑使用静态的工厂方法
▲3.6.11. 如果你需要访问外部的资源,考虑使用静态的工厂方法
▲3.6.12. 当面临非常多的参数的时候,考虑使用生成器(builder)
▲3.6.13. 当需要回避直接实例化类的时候使用考虑private的构造函数
▲3.6.14. 避免创建非必需的对象
▲3.6.15. 避免finalizer
▲3.6.16. 不要从无参的构造方法中抛出异常
▲3.6.17. 不要向一个已经发布的类中添加显示的构造方法
3.7. Setters和getters
▲3.7.1. 以get开头命名一个返回值不为boolean的访问属性的方法
▲3.7.2. 以is,can开头命名一个返回值为boolean的访问属性的方法
▲3.7.3. 以set开头命名一个更新本地变量的方法
▲3.7.4. 校验setter方法的参数
▲3.7.5. 最小化getter和setter方法的工作
▲3.7.6. 考虑从一个getter方法中返回不可变的集合
▲3.7.7. 考虑实现一个private接口的集合替代public的集合属性
▲3.7.8. 考虑只读的属性
▲3.7.9. 设置可变类型的属性时考虑Defensive Copy(Defensive Copy详见:http://www.javapractices.com/topic/TopicAction.doId=15)
▲3.7.10. 当返回可变类型的属性时考虑Defensive Copy
▲3.7.11. getter方法避免返回数组
▲3.7.12. 避免根据方法内信息无法完成的校验
▲3.7.13. 不要从getter方法中抛出异常
▲3.7.14. 不要设计只能set的属性方法(仅有public的setter而没有public的getter)
▲3.7.15. 不要依赖属性设置的顺序
3.8. 回调
▲3.8.1. 设计时使用最严密的预置条件
▲3.8.2. 设计时使用最弱的后置条件
▲3.8.3. 考虑传递引用对象的方法中把回调接口作为第一个参数
▲3.8.4. 避免有返回值的回调方法
3.9. 文档
▲3.9.1. 为每个方法提供Javadoc注释
▲3.9.2. 遵循标准的Javadoc约定
▲3.9.3. 每个方法以一句短小的话作为概述
▲3.9.4. 申明相关的方法
▲3.9.5. 用@deprecated标签申明过时的类型
▲3.9.6. 显示所有过时方法的替换方法
▲3.9.7. 避免冗长的zhus
▲3.9.8. 包含常用的使用模式
▲3.9.9. (如果允许的话)包含null值的确切含义
▲3.9.10. 包含方法的类型(无状态,访问器或者扩展)
▲3.9.11. 包含方法的预置条件
▲3.9.12. 包含算法实现的性能特征
▲3.9.13. 包含远程方法调用
▲3.9.14. 包含访问外部资源的方法
▲3.9.15. 包含哪些API可以在回调中使用
▲3.9.16. 考虑为了描述方法的行为而包含单元测试
PS:如果觉得我的分享不错,欢迎大家随手点赞、转发。