代码精进之路 核心知识 规范篇 命名篇

命名篇

1、有意思的命名

1.1、变量名

变量名应该是名词,能够正确地描述业务,有表达力。如果一个变量名需要注释来补充说明,那么很可能说明命名就有问题。

int d ; //标识过去的天数
int elapsedTimeInDays;

# 魔术数--见名知意
SECONDS_PER_DAY   -> 86400
PAGE_SIZE  -> 10

1.2、函数名

函数命名要具体,空泛的命名没有意义。

processData()   差于  validateUserCredentials() 或 eliminateDuplicateRequests()

getLatestEmployee()  好于 popRecord()

1.3、类名

类是面向对象中最重要的概念之一,是一组数据和操作的封装。对于一个应用系统,我们可以将类分为两大类:实体类辅助类

实体类承载了核心业务数据和核心业务逻辑,其命名要充分体现业务语义,并在团队内达成共识,如CustomerBankEmployee等。

辅助类是辅佐实体类一起完成业务逻辑的,其命名要能够通过后缀来体现功能。例如,用来为Customer做控制路由的控制类CustomerController、提供Customer服务的服务类CustomerService、获取数据存储的仓储类CustomerRepository

对于辅助类,尽量不要用HelperUtil之类的后缀,因为其含义太过笼统,容易破坏SRP(单一职责原则)。比如对于处理CSV,可以这样写:

CSVHelper.parse(String)
CSVHelper.create(int[])

但是我更建议将CSVHelper拆开:

CSVParser.parse(String)
CSVBuilder.create(int[])

1.4、包名

包(Package)代表了一组有关系的类的集合,起到分类组合和命名空间的作用。

包名应该能够反映一组类在更高抽象层次上的联系。例如,有一组类ApplePearOrange,我们可以将它们放在一个包中,命名为fruit

包的命名要适中,不能太抽象,也不能太具体。此处以上面提到的水果作为例子,如果包名过于具体,比如Apple,那么Pear和Orange放进该包中就不恰当了;如果报名太抽象,称为Object,而Object无所不包,这就失去了包用来限定范围的作用。

1.5、模块名

相对于包来说,模块的粒度更大,通常一个模块中包含了多个包。

名称要反映模块在系统中的职责。因此,对任何应该遵循COLA规范的应用都有着xxx-controllerxxx-appxxx-domainxxx-Infrastructure这4个标准模块。

2、保持一致性

保持命名的一致性,可以提高代码的可读性,从而简化复杂度。因此,我们要小心选择命名,一旦选中,就要持续遵循,保证名称始终一致。

2.1、每个概念一个词

每个概念对应一个词,并且一以贯之。

例如,fetch、retrieve、get、find和query都可以表示查询的意思,如果不加约定地给多个类中的同种查询方法命名,你怎么记得是哪个类中的哪个方法呢?同样,在一段代码中,同时存在manager、controller和handler,会令人感到困惑。

在实际项目中,按照以下约定,保持命名的一致性:

CURD操作 方法名约定
新增 create
添加 add
删除 remove
修改 update
查询(单个结果) get
查询(多个结果) list
分页查询 page
统计 count

2.2 使用对仗词

遵守对仗词的命名规则有助于保持一致性,从而提高代码的可读性。
下面列出一些常见的对仗词组:

  • 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

2.3 后置限定词

如果你要用类似TotalSumAverageMaxMin这样的限定词来修改某个命名,那么记住把限定词加到名字的最后,并在项目中贯彻执行,保持命名风格的一致性。

这种方法有很多优点。首先,变量名中最重要的部分,即为这一变量赋予主要含义的部分应位于最前面,这样可以突出显示,并会被首先阅读到。其次,可以避免同时在程序中使用totalRevenuerevenueTotal而产生的歧义。如果贯彻限定词后置的原则,我们就能收获一组非常优雅、具有对称性的变量命名,例如revenueTotal(总收入)、expenseTotal(总支出)、revenueAverage(平均收入)和expenseAverage(平均支出)。

需要注意的一点是Num这个限定词,Num放在变量名的结束位置表示一个下标,customerNum表示的是当前客户的序号。为了避免Num带来的麻烦,我建议用Count或者Total来表示总数,用Id表示序号。这样,customerCount表示客户的总数,customerId表示客户的编号。

3、自明的代码

3.1、中间变量

我们可以通过添加中间变量让代码变得更加自明,即将计算过程打散成多个步骤,并用有意义的变量名来命名中间变量,从而把隐藏的计算过程以显性化的方式表达出来。

例如,我们要通过Regex来获得字符串中的值,并放到map中。

Marcher matcher = headerPattern.matcher(line);
if(matcher.find()) {
  headers.put(matcher.group(1), matcher.group(2));
}

用中间变量,可以写成如下形式:

Marcher matcher = headerPattern.matcher(line);
if(matcher.find()) {
  String key = matcher.group(1);
  String value = matcher.group(2);
  headers.put(key, value);
}

中间变量的这种简单用法,显性地表达了第一个匹配组是key,第二个匹配组是value。只要把计算过程打散成一系列良好命名的中间值,不透明的语义自然会变得透明。

3.2、设计模式语言

使用设计模式语言也是代码自明的重要手段之一,在技术人员之间共享和使用设计模式语言,可以极大地提升沟通的效率。当然,前提是大家都要理解和熟悉这些模式,否则就会变成“鸡同鸭讲”。因此,我们有必要在命名上就将设计模式显性化出来,这样阅读代码的人能很快领会到设计者的意图。

例如,Spring里面的ApplicationListener就充分体现了它的设计和用处。通过这个命名,我们知道它使用了观察者模式,每一个被注册的ApplicationListenerApplication状态发生变化时,都会接收到一个notify。这样我们就可以在容器初始化完成之后进行一些业务操作,比如数据加载、初始化缓存等。

3.3、小心注释

  • 不要复述功能
  • 要解释背后意图

4、规范

4.1、命名规范

在Java中,我们通常使用如下命名约定。

  • 类名采用“大驼峰”形式,即首字母大写的驼峰,例如ObjectStringBufferFileInputStream
  • 方法名采用“小驼峰”形式,即首字母小写的驼峰,方法名一般为动词,与参数组成动宾结构,例如Threadsleep(long millis)StringBufferappend(String str)
  • 常量命名的字母全部大写,单词之间用下划线连接,例如TOTAL_COUNTPAGE_SIZE等。
  • 枚举类以EnumType结尾,枚举类成员名称需要全大写,单词间用下划线连接,例如SexEnum.MALESexEnum.FEMALE
  • 抽象类名使用Abstract开头;异常类使用Exception结尾;实现类以Impl结尾;测试类以它要测试的类名开始,以Test结尾。

4.2、日志规范

详细的日志输出级别分为OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL或者自定义的级别。我认为比较有用的4个级别依次是ERROR、WARN、INFO和DEBUG。通常这4个级别就能够很好地满足我们的需求了。

  • 1.ERROR级别
    ERROR表示不能自己恢复的错误,需要立即被关注和解决。例如,数据库操作错误、I/O错误(网络调用超时、文件读取错误等)、未知的系统错误(NullPointerException、OutOfMemoryError等)。
    对于ERROR,我们不仅要打印线程堆栈,最好打印出一定的上下文(链路TraceId、用户Id、订单Id、外部传来的关键数据),以便于排查问题。
    ERROR要接入监控和报警系统。ERROR需要人工介入处理,及时止损,否则会影响系统的可用性。当然也不能滥用ERROR,否则就会出现“狼来了”的情况。我在实际工作中曾碰到过系统每天会发出上千条错误报警的情况,导致根本没有人看报警内容,在真正出现问题时,也没有人关注,从而引发线上故障。因此,一定要做好ERROR输出的场景定义和规范,再配合监控治理,双管齐下,确保线上系统的稳定。
  • 2.WARN级别
    对于可预知的业务问题,最好不要用ERROR输出日志,以免污染报警系统。例如,参数校验不通过、没有访问权限等业务异常,就不应该用ERROR输出。
    需要注意的是,在短时间内产生过多的WARN日志,也是一种系统不健康的表现。因此,我们有必要为WARN配置一个适当阈值的报警,比如访问受限WARN超过100次/分,则发出报警。这样在WARN日志过于频繁时,我们能及时收到系统报警,去跟进用户问题。例如,如果是产品设计上有缺陷导致用户频繁出现操作卡点,可以考虑做一下流程或者产品上的优化。
  • 3.INFO级别
    INFO用于记录系统的基本运行过程和运行状态。
    通常来说,优先根据INFO日志可初步定位,主要包括系统状态变化日志、业务流程的核心处理、关键动作和业务流程的状态变化。适当的INFO可以协助我们排查问题,但是切忌把INFO当成DEBUG使用,这样会导致记录的数据过多,一方面影响系统性能,日志文件增长过快,消耗不必要的存储资源;另一方面也不利于阅读日志文件。
  • 4.DEBUG级别
    DEBUG是输出调试信息,如request/response的对象内容。在输出对象内容时,要覆盖Object的toString方法,否则输出的是对象的内存地址,就起不到调试的作用了。通常在开发和预发环境下,DEBUG日志会打开,以方便开发和调试。而在线上环境,DEBUG开关需要关闭,因为在生产环境下开启DEBUG会导致日志量非常大,其损耗是难以接受的。只有当线上出现bug或者棘手的问题时,才可以动态地开启DEBUG。为了防止日志量过大,我们可以采用分布式配置工具来实现基于requestId判断的日志过滤,从而只打印我们所需请求的DEBUG日志。

4.3、异常规范

4.3.1、异常处理

建议在业务系统中设定两个异常,分别是BizException(业务异常)和SysException(系统异常),而且这两个异常都应该是UncheckedException。

为什么不建议用Checked Exception呢?

因为它破坏了开闭原则。如果你在一个方法中抛出了Checked Exception,而catch语句在3个层级之上,那么你就要在catch语句和抛出异常处理之间的每个方法签名中声明该异常。这意味着在软件中修改较低层级时,都将波及较高层级,修改好的模块必须重新构建、发布,即便它们自身所关注的任何东西都没有被改动过。这也是C#、Python和Ruby语言都不支持Checked Exception的原因,因为其依赖成本要高于显式声明带来的收益。

最后,针对业务异常和系统异常要做统一的异常处理,类似于AOP,在应用处理请求的切面上进行异常处理收敛,其处理流程如下:

try {
  // 业务处理
Response res = process(request);
} catch(BizException e) {
  // 业务异常使用 warn 级别
  logger.warn("BizException with error code:{}, error message:{}", e.getErrorCode, e.getErrorMeg());
}catch (SysException ex){
  //系统异常使用 error 级别
  logger.error("System error" + ex.getMessage(), ex);
}catch (Exception ex) {
  //兜底
  logger.error("System error" + ex.getMessage(), ex);
}

千万不要在业务处理内部到处使用try/catch打印错误日志,这样会使功能代码和业务代码缠绕在一起,让代码显得很凌乱,并且影响代码的可读性。

4.3.2、错误码

(1)编号错误码

对于平台、底层系统或软件产品,可以采用编号式的编码规范,好处是编码风格固定,给人一种正式感;缺点是必须要配合文档才能理解错误码代表的意思。

例如,数据库软件Oracle中总共有2000多个异常,其编码规则是ORA-00001~ORA-02149,每一个错误码都有对应的错误解释。
ORA-00001:违反唯一约束条件。
ORA-00017:请求会话以设置跟踪事件。
ORA-00018:超出最大会话数。
ORA-00019:超出最大会话许可数。
ORA-00023:会话引用进程私用内存;无法分离会话。
ORA-00024:单一进程模式下不允许从多个进程注册。

要注意,对不同的错误波段,一定要预留足够的码号。例如,淘宝开放平台所用的3位数就显得有些拘谨,其支撑的错误数最多不能超过100,超过100后,为了向后兼容,只能通过子错误码的方式进行变通处理。

(2)显性化错误码

显性化的错误码具有更强的灵活性,适合敏捷开发。例如,我们可以将错误码定义成3个部分:类型+场景+自定义标识。每个部分之间用下划线连接,内容以大驼峰的方式书写。这里可以打破Java的常量命名规范,驼峰方式会更方便阅读。

对于错误类型,我们可以做一个约定:P代表参数异常(ParamException)、B代表业务异常(BizException)、S代表系统异常(SystemException)。一个完整的示例如表2-1所示。

错误类型 错误码约定 举例
参数异常 P_XX_XX P_Customer_NameNull:客户姓名不能为空
业务异常 B_XX_XX B_Customer_NameAlreadyExist:客户姓名已存在
系统异常 S_XX_XX S_Unknow_Error:未知系统错误

如果业务应用的错误都用这种约定来描述和表达,那么只要大家都遵守相同的规范,系统的可维护性和可理解性就会大大提升。

4.4、埋点规范

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章