前言
一个好的命名,能够让代码的概念清晰, 增加代码的表达力,让代码更易懂。命名并不是一个简单的事情,命名的过程本身就是一个抽象和思考的过程,当我们不能给一个模块、一个对象、一个函数等找到合适的名称的时候, 说明我们对问题理解的还不够透彻,需要重新去挖掘问题的本质, 对问题域进行重新分析和抽象,甚至需要重新设计和重构代码。
本文主要是总结不同文章中提及的关于命名的一些重点,学习以提高代码规范性。
代码精进之路:从码农到工匠
本小节主要是总结代码精进之路:从码农到工匠的第一章命名,若想查看全部内容, 请查看对应的文章。
命名其实是一件很难的事情, 因为命名的过程本身就是一个抽象和思考的过程,在工作中, 当我们不能给一个模块、一个对象、一个函数,甚至一个变量找到合适的名称的时候, 往往说明我们对问题的理解还不够透彻,需要重新去挖掘问题的本质, 对问题域进行重新分析和抽象, 有时还要调整设计和重构代码。因为好的命名是我们写出好代码的基础。一个好的命名可以起到代码即文档的作用,如果想不到一个合适的名字, 意味着代码存在“坏味道”, 设计有问题。
1. 有意义的命名
1.1 变量名
变量名应该是名词,能够正确地描述业务, 有表达力。 如果一个变量名需要注释来说明, 那么很可能说明命名就有问题。
int d; //表示过去的天数
上面的命名很明显就有问题, 以下是好的命名
int elapsedTimeInDays;
int secondsPerDay = 86400;
int pageSize = 10;
这样做还有一个好处, 即代码的可搜索性,在代码中查找pageSize很容易, 但是想找到10就很麻烦了, 因为它可能是某些注释或者常量定义的一部分, 出现在不同作用的各种表达式中。
1.2 函数名
函数命名要具体,空泛的命名没有意义。例如processData()就不是一个好的命名, 因为所有的方法都是对数据的处理,相比之下,validateUserCredentials()就好很多。
函数命名要体现做什么, 而不是怎么做,并且命名应该提升抽象层次, 体现业务语义。
例如将把雇员信息存到栈中,现在要获取最近存储的一个雇员信息, 显然getLastestEmployee()就比popRecord()要好,屏蔽了底层细节。
1.3 类名
类是对数据和操作的封装, 对于一个应用系统, 可以将类分为两大类:实体类和辅助类。
实体类承载了核心业务数据和核心业务逻辑, 其命名要能充分体现业务语义并在团队内达成共识,如Customer,Bank和Employee等
辅助类是辅佐实体类一起完成业务逻辑的,其命名要能通过后缀来体现功能。如控制路由的控制类CustomerController。 提供服务的CustomerService和进行数据库交互的CustomerRepository。
对于辅助类, 尽量不要用Helper、Util之类的后缀, 因为其含义太过笼统, 容易破坏SRP(单一职责原则)。例如对于CSV的处理可以从
CSVHelper.parse(String)
CSVHelper(int[])
变成
CSVParser.parse(String)
CSVBuilder.create(int[])
1.4 包名
包(Package)代表了一组有关系的类的集合,起到分类组合和命名空间的作用。
包名应该能够反映一组类在更高抽象层次上的联系。 例如,有一组类Apple、Pear、Orange,我们可以将他们放到一个包中, 命名为fruit。包的命名要适中, 不能太抽象, 也不能太具体。
1.5 模块名
这里指的模块通常是Maven中的Module, 相对于包来说, 模块的粒度更大, 通常一个模块包含多个包。在Maven中一个模块对应一个坐标:<groupId, artifactId>。名称要保证在Maven仓库中的唯一性也要反映模块在系统中的职责。在COLA架构中,模块代表着架构层次。如下图所示
关于cola4.0具体的规范可以查看cola作者对应文章
1.6 保持一致性
保持命名的一致性, 可以提高代码的可读性, 进而简化复杂度。
1.6.1 每个概念一个词
每个概念对应一个词, 并且一以贯之。例如fetch, retrieve, get, find和query都可以表示查询的意思, 如果不加约定就会产生迷惑。
在代码精进之路中是这样定义的
1.6.2 使用对仗词
遵循对仗词的命名规则有助于保持一致性, 从而提高代码的可读性。 像first/last这样的对仗词就很容易理解, 而像fileOpen()和fClose()这样的组合则不对称, 容易使人迷惑。以下是常见的一些对仗词组。
- add/remove
- increment/decrement
- open/close
- begin/end
- insert/delete
- show/hide
- create/destroy
- lock/unlock
- source/target
- first/last
- min/max
- start/stop
- get/set
- next/previous
- up/down
- old/new
1.6.3 后置限定词
多程序中会有表示计算结果的变量,例如总额、平均值、最大值等。如果你要用类似Total、Sum、Average、Max、Min这样的限定词来修改某个命名,那么记住把限定词加到名字的最后,并在项目中贯彻执行,保持命名风格的一致性。
这种方法有很多优点。
- 首先,变量名中最重要的部分,即为这一变量赋予主要含义的部分应位于最前面,这样可以突出显示,并会被首先阅读到。
- 其次,可以避免同时在程序中使用totalRevenue和revenueTotal而产生的歧义。如果贯彻限定词后置的原则,我们就能收获一组非常优雅、具有对称性的变量命名,例如revenueTotal(总收入)、expenseTotal(总支出)、revenueAverage(平均收入)和expenseAverage(平均支出)。
需要注意的一点是Num这个限定词,Num放在变量名的结束位置表示一个下标,customerNum表示的是当前客户的序号。为了避免Num带来的麻烦,我建议用Count或者Total来表示总数,用Id表示序号。这样,customerCount表示客户的总数,customerId表示客户的编号。
1.6.4 统一业务语言
统一语言就是要确保团队在内部的所有交流、模型、代码和文档中都要使用同一种编程语言。 实际上,统一语言(Ubiquitous Language)也是领域驱动设计(Domain Driven Design,DDD)中的重要概念。
1.6.5 统一技术语言
有些技术语言是通用的,业内人士都能理解,我们应该尽量使用这些术语来进行命名。这些通用技术语言包括DO、DAO、DTO、ServiceI、ServiceImpl、Component和Repository等。例如,在代码中看到OrderDO和OrderDAO,马上就能知道OrderDO中的字段就是数据库中Order表字段,对Order表的操作都在OrderDAO里面。
2. 自明的代码
“好的代码是最好的文档”。也就是说,代码若要具备文档的功能,前提必须是其本身要具备很好的可读性和自明性。所谓自明性,就是在不借助其他辅助手段的情况下,代码本身就能向读者清晰地传达自身的含义。
2.1 中间变量
我们可以通过添加中间变量让代码变得更加自明,即将计算过程打散成多个步骤,并用有意义的变量名来命名中间变量,从而把隐藏的计算过程以显性化的方式表达出来。
例如,我们要通过Regex来获得字符串中的值,并放到map中。
Matcher matcher = headerPattern.matcher(line);
if(matcher.find()){
headers.put(matcher.group(1), matcher.group(2));
}
用中间变量,可以写成如下形式:
Matcher matcher = headerPattern.matcher(line);
if(matcher.find()){
String key = matcher.group(1);
String value = matcher.group(2);
headers.put(key, value);
}
中间变量的这种简单用法,显性地表达了第一个匹配组是key,第二个匹配组是value。只要把计算过程打散成一系列良好命名的中间值,不透明的语义自然会变得透明。
2.2 设计模式语言
使用设计模式语言也是代码自明的重要手段之一,在技术人员之间共享和使用设计模式语言,可以极大地提升沟通的效率。当然,前提是大家都要理解和熟悉这些模式,否则就会变成“鸡同鸭讲”。因此,我们有必要在命名上就将设计模式显性化出来,这样阅读代码的人能很快领会到设计者的意图。
例如,Spring里面的ApplicationListener就充分体现了它的设计和用处。通过这个命名,我们知道它使用了观察者模式,每一个被注册的ApplicationListener在Application状态发生变化时,都会接收到一个notify。这样我们就可以在容器初始化完成之后进行一些业务操作,比如数据加载、初始化缓存等。
又如,在进行EDM(邮件营销)时要根据一些规则过滤掉一些客户,比如没有邮箱地址的客户、没有订阅关系不能发送邮件的客户、3天内不能重复发送邮件的客户等。
下面是一个典型的pipeline处理方式,责任链在处理该问题上是一个很好的选项,FilterChain这个名字非常恰当地表达出了作者的意图,Chain表示用的是责任链模式,Filter表示用来进行过滤。
FilterChain filterChain = FilterChainFactory.buildFilterChain(
NoEmailAddressFilter.class,
EmailUnsubscribeFilter.class,
EmailThreeDayNotRepeatFilter.class);
//具体的Filter
public class NoEmailAddressFilter implements Filter {
@Override
public void doFilter(Object context, FilterInvoker nextFilter) {
Map<String, Object> contextMap = (Map<String, Object>)context;
String email = ConvertUtils.convertParamType (contextMap.get("email"), String.class);
if(StringUtils.isBlank(email)){
return;
}
nextFilter.invoke(context);
}
}
2.3 小心注释
如果注释是为了阐述代码背后的意图,那么这个注释是有用的;如果注释是为了复述代码功能,那么就要小心了,这样的注释往往意味着“坏味道”(在Martin Fowler的《重构:改善既有代码的设计》一书中,注释就是“坏味道”之一),是为了弥补我们代码表达能力的不足。就像Brian W.Kernighan说的那样:“别给糟糕的代码加注释——重新写吧。”
2.3.1 不要复述功能
为了复述代码功能而存在的注释,主要作用是弥补我们表达意图时遭遇的失败,这时要考虑这样的注释是否是必需的。如果编程语言足够有表达力,或者我们擅长用代码显性化地表达意图,那么也许根本就不需要注释。 因此,在写注释时,你应该自省自己是否在表达能力上存在不足,真正的高手是尽量不写注释。
在JDK的源码java.util.logging.Handler中,我们可以看到如下代码:
public synchronized void setFormatter(Formatter newFormatter) {
checkPermission();
// Check for a null pointer:
newFormatter.getClass();
formatter = newFormatter;
}
如果没有注释,那么可能没人知道“newFormatter.getClass();”是为了判空,注释“Check for a null pointer”就是为了弥补代码表达能力的失败而存在的。如果我们换一种写法,使用java.util.Objects.requireNonNull进行判空,那么注释就完全是多余的,代码本身足以表达其意图。
2.3.2 要解释背后意图
注释要能够解释代码背后的意图,而不是对功能的简单重复。例如,我们在一个系统中看到如下代码:
try {
//在这里等待2秒
Thread.sleep(2000);
} catch (InterruptedException e) {
LOGGER.error(e);
}
这里的注释和没写是一样的,因为它只是对sleep的简单复述。正确的做法应该是阐述sleep背后的原因,比如改写成如下形式就会好很多。
try {
//休息2秒,为了等待关联系统处理结果
Thread.sleep(2000);
} catch (InterruptedException e) {
LOGGER.error(e);
}
或者直接用一个private方法将其封装起来,用显性化的方法名来表达意图,这样就不需要注释了。
private void waitProcessResultFromA( ){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
LOGGER.error(e);
}
}
阿里巴巴开发手册
命名风格和常量定义
本小节主要是总结阿里巴巴Java开发手册1.7.0(嵩山版)编码规范-命名风格和常量定义部分内容,若想查看全部内容, 请自行查看对应的文章。
-
【强制】类名使用 UpperCamelCase 风格,但以下情形例外:DO / BO / DTO / VO / AO / PO / UID 等。
正例:ForceCode / UserDO / HtmlDTO / XmlService / TcpUdpDeal / TaPromotion
反例:orcecode / UserDo / HTMLDto / XMLService / TCPUDPDeal / TAPromotion
-
【强制】常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。
正例:MAX_STOCK_COUNT / CACHE_EXPIRED_TIME
反例:MAX_COUNT / EXPIRED_TIME
-
【强制】抽象类命名使用 Abstract 或 Base 开头;异常类命名使用 Exception 结尾;测试类
命名以它要测试的类的名称开始,以 Test 结尾。
-
【强制】POJO 类中的任何布尔类型的变量,都不要加 is 前缀,否则部分框架解析会引起序列
化错误。
说明:在本文 MySQL 规约中的建表约定第一条,表达是与否的变量采用 is_xxx 的命名方式,所以,需要
在
<resultMap>设置从 is_xxx 到 xxx 的映射关系。
反例:定义为基本数据类型 Boolean isDeleted 的属性,它的方法也是 isDeleted(),框架在反向解析的时
候,“误以为”对应的属性名称是 deleted,导致属性获取不到,进而抛出异常。
-
【强制】包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用
单数形式,但是类名如果有复数含义,类名可以使用复数形式。
正例:应用工具类包名为 com.alibaba.ei.kunlun.aap.util、类名为 MessageUtils(此规则参考 spring 的
框架结构)
-
【推荐】在常量与变量的命名时,表示类型的名词放在词尾,以提升辨识度。
正例:startTime / workQueue / nameList / TERMINATED_THREAD_COUNT
反例:startedAt / QueueOfWork / listName / COUNT_TERMINATED_THREAD
-
【推荐】如果模块、接口、类、方法使用了设计模式,在命名时需体现出具体模式。
说明:将设计模式体现在名字中,有利于阅读者快速理解架构设计理念。
正例:
public class OrderFactory; public class LoginProxy; public class ResourceObserver; -
【推荐】接口类中的方法和属性不要加任何修饰符号(public 也不要加),保持代码的简洁
性,并加上有效的 Javadoc 注释。尽量不要在接口里定义变量,如果一定要定义变量,确定
与接口方法相关,并且是整个应用的基础常量。
正例:接口方法签名 void commit();
接口基础常量 String COMPANY = "alibaba";
反例:接口方法定义 public abstract void f();
说明:JDK8 中接口允许有默认实现,那么这个 default 方法,是对所有实现类都有价值的默认实现。
-
接口和实现类的命名有两套规则:
1)【强制】对于 Service 和 DAO 类,基于 SOA 的理念,暴露出来的服务一定是接口,内部的实现类用
Impl 的后缀与接口区别。
正例:CacheServiceImpl 实现 CacheService 接口。
2)【推荐】如果是形容能力的接口名称,取对应的形容词为接口名(通常是–able 的形容词)。
正例:AbstractTranslator 实现 Translatable 接口。
-
【参考】枚举类名带上 Enum/Type 后缀,枚举成员名称需要全大写,单词间用下划线隔开。
说明:枚举其实就是特殊的常量类,且构造方法被默认强制是私有。
正例:枚举名字为 ProcessStatusEnum 的成员名称:SUCCESS / UNKNOWN_REASON。
-
【推荐】不要使用一个常量类维护所有常量,要按常量功能进行归类,分开维护。
说明:大而全的常量类,杂乱无章,使用查找功能才能定位到修改的常量,不利于理解,也不利于维护。
正例:缓存相关常量放在类 CacheConsts 下;系统配置相关常量放在类 SystemConfigConsts 下。
-
【推荐】常量的复用层次有五层:跨应用共享常量、应用内共享常量、子工程内共享常量、包
内共享常量、类内共享常量。
1) 跨应用共享常量:放置在二方库中,通常是 client.jar 中的 constant 目录下。
2) 应用内共享常量:放置在一方库中,通常是子模块中的 constant 目录下。
反例:易懂变量也要统一定义成应用内共享常量,两位工程师在两个类中分别定义了“YES”的变量:
类 A 中:public static final String YES = "yes";
类 B 中:public static final String YES = "y";
A.YES.equals(B.YES),预期是 true,但实际返回为 false,导致线上问题。
3) 子工程内部共享常量:即在当前子工程的 constant 目录下。
4) 包内共享常量:即在当前包下单独的 constant 目录下。
5) 类内共享常量:直接在类内部 private static final 定义。
-
【推荐】如果变量值仅在一个固定范围内变化用 enum 类型来定义。
说明:如果存在名称之外的延伸属性应使用 enum 类型,下面正例中的数字就是延伸信息,表示一年中的
第几个季节。
例子:
public enum SeasonEnum { SPRING(1), SUMMER(2), AUTUMN(3), WINTER(4); private int seq; SeasonEnum(int seq) { this.seq = seq; } public int getSeq() { return seq; } }
专有名词解释
- POJO(Plain Ordinary Java Object):在本规约中,POJO 专指只有 setter/getter/toString 的
简单类,包括 DO/DTO/BO/VO 等。
- DO(Data Object):阿里巴巴专指数据库表一一对应的 POJO 类。此对象与数据库表结构一
一对应,通过 DAO 层向上传输数据源对象。
- DTO(Data Transfer Object):数据传输对象,Service 或 Manager 向外传输的对象。
- BO(Business Object):业务对象,可以由 Service 层输出的封装业务逻辑的对象。
- Query:数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止使用
Map 类来传输。
- VO(View Object):显示层对象,通常是 Web 向模板渲染引擎层传输的对象。
- AO(Application Object): 阿里巴巴专指 Application Object,即在 Service 层上,极为贴近
业务的复用代码。
- CAS(Compare And Swap):解决多线程并行情况下使用锁造成性能损耗的一种机制,这是
硬件实现的原子操作。CAS 操作包含三个操作数:内存位置、预期原值和新值。如果内存位
置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何
操作。
- GAV(GroupId、ArtifactId、Version): Maven 坐标,是用来唯一标识 jar 包。
10.OOP(Object Oriented Programming): 本文泛指类、对象的编程处理方式。
11.AQS(AbstractQueuedSynchronizer): 利用先进先出队列实现的底层同步工具类,它是很多上
层同步实现类的基础,比如:ReentrantLock、CountDownLatch、Semaphore 等,它们通
过继承 AQS 实现其模版方法,然后将 AQS 子类作为同步组件的内部类,通常命名为 Sync。
12.ORM(Object Relation Mapping): 对象关系映射,对象领域模型与底层数据之间的转换,本
文泛指 iBATIS, mybatis 等框架。
13.NPE(java.lang.NullPointerException): 空指针异常。
14.OOM(Out Of Memory): 源于 java.lang.OutOfMemoryError,当 JVM 没有足够的内存
来为对象分配空间并且垃圾回收器也无法回收空间时,系统出现的严重状况。
15.一方库: 本工程内部子项目模块依赖的库(jar 包)。
16.二方库: 公司内部发布到中央仓库,可供公司内部其它应用依赖的库(jar 包)。
17.三方库: 公司之外的开源库(jar 包)。
参考
- 阿里巴巴Java开发手册1.7.0(嵩山版)编码规范
- 代码精进之路:从码农到工匠 第一章命名