如何写出整洁代码是一名优秀程序员的必修课。
整洁的代码有助于项目提高迭代质量,减轻历史包袱;同时也能让别人更易看懂代码,促进团队协作分工。
《代码整洁之道》一书里边有不少观点值得采纳。本文摘录书本主要内容,以备后忘。详细建议阅读原著。
有意义的命名
- 命名应当名副其实,能准确体现对象的含义;
- 避免误导性命名。比如:不要使用”l”、”1” 以及其他专有名称;
- 命名应该做有意义的区分。比如:ProductInfo 、ProductData和Product没区别,moneyAmount和money没区别;
- 使用可以读出来的命名。比如:使用generationTimestamp,而不是genymdhms;
- 使用可以搜索的命名。比如:”e” 或者魔法数在代码中难以搜索;
- 避免使用编码(包括匈牙利标记法、成员前缀或接口实现标记等等)。比如:IShapeFactory无论是作为子类或者其实现,其首字母的标记”I”都是累赘的,应该使用ShapeFactory;
- 类名或者对象名应该是名词,如:Customer、WikiPage、Account、AddressParser。避免使用含义不确切的名词:Manager、Processor、Data、Info等,更不应使用动词;
- 方法名应该是动词或者动词短语;
- 添加有意义的语境。比如:firstName、secondName、street、houseNumber等零散字段,可以增加前缀”addr-“作为语境,明确暗示它们构成一个地址;更好的办法是封装为Address类;
- 不要添加无用的语境。比如开发一个名为GSD的系统,不应该给所有的类都加上GSD前缀;
函数
- 函数第一规则:短小
- 函数的缩进层次要控制的两层之内;
- 函数应该做一件事,做好这件事,只做一件事;
- 使用抽象工厂类隐藏switch语句;
- 使用描述性命名,长而具有描述性的命名,要比短而令人费解的命名要好;
- 函数参数越少越好,当参数数量需要两个、三个或三个以上的时候,就说明其中一些参数可能应该封装为类;
- 标志参数丑陋不堪,不应该向函数传入布尔值,而是拆分为两个方法。比如:setStatus(Boolean start) 应该拆分为 start() 和 stop();
- 分隔询问和指令。比如if (set(“username”, “unclebob”)… 应该拆分为:
if (attributeExists("username") { setAttribute("username", "unclebob");` ... }
- 使用异常替代返回错误码;
- try/catch代码块会搞乱代码结构,应该将try和catch代码块的主体部分抽离出来形成独立函数。比如:
try { deletePageAndAllReferences(page); } catch (Exception e) { logError(e); }
- 避免重复的代码;
注释
恰当的注释是用来弥补我们用代码表达意图时所遭遇的失败。如果发现自己需要写注释,先复盘下是否可以通过代码表达而不是注释;
注释不能美化糟糕的代码。与其花时间编写解释糟糕代码的注释,还不如花时间清理下糟糕的代码;
通过良好的代码来阐述逻辑。比如:
- 良好代码:
if (employee.isEligibleForFullBenefits())
- 而不要采用糟糕注释:
// check to see if the employee is eligible for full benefits if ((employee.flags & HOURLY_FLAG) && employee.age > 65))
好注释
- 法律信息
- 提供信息的注释
- 对意图的解释
- 阐释
- 警示
- TODO注释
- 放大
- 公共API的Javadoc
坏注释
- 喃喃自语
- 多余的注释
- 误导性注释
- 循规式注释
- 日志式注释
- 废话式注释
- 注释掉的代码
- HTML注释
- 非公共API的Javadoc
格式
- 单个文件控制在恰当的长度(比如200~500行);
- 源文件应该像报纸一样。名称应该简单且一目了然。名称本身应该足够告诉我们是否在正确的模块中。源文件最顶部应该给出高层次概念和算法。细节应该往下渐次展开,直至找到源文件中最底层的函数和细节。
- 源文件内部在垂直方向上,不同概念之间应该有间隔。比如:import块、import static块、变量定义、方法区等等,它们之间应该有空行间隔;
- 紧密相关的代码应该相互靠近。比如:
- 不必在变量之间插入无用的注释;
- 变量声明应该尽可能靠近其实用的位置;
- 循环中的控制变量应该尽量在循环语句中声明;
- 被调用的函数应该在调用函数的下方;
- 概念相关的代码应该放在一起;
- 每行代码长度控制在120个字符以内;
- 代码行中恰当使用空格。比如:逗号后加一个空格;
- 统一的缩进风格;
对象和数据结构
- 数据抽象以避免曝露数据细节,以抽象形态表述数据;
- 过程式代码便于在不改动既有数据机构的前提下添加新函数。面向对象代码便于在不改动既有函数的前提下添加新类;
- 反之亦然,过程式代码难以添加新数据结构,因为必须修改所有函数。面向对象代码难以添加新函数,因为必须修改所有类;
错误处理
- 使用异常而非返回码;
- 不要在方法中返回null值,而是抛出异常或返回特例对象;
- 不要向方法传递null值;
边界
单元测试
- 不要遵循TDD三定律(所谓的编写生产代码前先编写单元测试);
- 保持测试代码的整洁,抽象隐藏测试准备细节,突出测试逻辑;
- 单元测试中优先考虑简介,再考虑执行效率;
- 每个测试有且只有一个断言;
类
- 类应该保持短小;
- 类应该遵守单一权责原则(SRP);
- 类内部应该保持高内聚(保持每个方法都操作内部的一个或者多个变量);
系统
系统的构造和使用分开的办法:
- 分离构造过程,创建系统所需的对象,并传递给应用程序,应用程序只管使用;
- 在应用程序中使用抽象工厂方法来隐藏构建细节;
- 使用依赖注入来分离构造和使用;
Java AOP三种机制:
- Java代理,常用的字节码操作库有:CGLIB、ASM、Javassist;
- 纯Java AOP框架;
- AspectJ;
迭进
- 运行所有测试;
- 不可重复;
- 调整代码,确保具有良好的表达力;
- 尽可能少的类和方法;
并发编程
- 对象是过程的抽象,线程是调度的抽象;
- 并发防御性原则
- 单一权责原则;
- 限制数据作用域;
- 使用数据副本;
- 线程应尽可能独立;
- 了解Java并发安全库;
- 了解并发执行模型;
- 保持同步区域微小;
味道与启发
注释
- 不恰当的信息
- 废弃的注释
- 冗余的注释
- 糟糕的注释
- 注释掉的代码
环境
- 需要多步才能实现的构建
- 需要多步才能做到的测试
函数
- 过多的参数
- 输出参数
- 标志参数(布尔值参数)
- 不被调用的死函数
一般性问题
- 一个源文件存在多种语言
- 明显的行为未被实现
- 不正确的边界行为
- 忽视安全(防御性编程)
- 重复代码
- 在错误的抽象层级上的代码
- 基类依赖于派生类
- 信息过多(类中方法过多、函数变量过多等等)
- 不执行的代码
- 垂直距离过大(变量、函数应该在靠近使用的地方定义)
- 前后不一致(不同地方的相似概念应该用相似一致的命名,如方法名processVerificationRequest和processDeletionRequest)
- 混淆视听的代码(如:没有实现的默认构造函数、没用的变量、无调用的函数、无信息量的注释)
- 人为耦合。(不相互依赖的东西不应该耦合)
- 特性依恋。(类的方法只应对所属类中的变量和函数感兴趣,不应该垂青其他类中的变量和函数)
- 选择算子参数。选择算子可能是boolean、枚举、整数等形式,通常可以拆分为多个小函数
- 晦涩的意图,包括联排表达式、匈牙利语标记法、魔术数等
- 位置错误的权责
- 不恰当的静态方法。如果的确需要静态方法,那么请确保没机会打算让它有多态行为
- 使用解释性变量
- 函数名称应该表达其行为
- 理解算法
- 把逻辑依赖改为物理依赖。(比如某些写死的静态变量,改为从实体的get方法获取)
- 用多态替代If/Else或Switch/Case
- 团队成员应遵循标准约定
- 用命名常量替代魔术数
- 封装布尔条件
- 避免否定性布尔条件
- 函数只做一件事
- 掩蔽时序耦合
- 封装边界条件(比如level+1、level-1)
- 函数应该只在一个抽象层级
- 在较高层级放置可配置数据
Java
- 通过使用通配符避免过长的导入清单(import)
- 不要继承常量
- 优先使用枚举而不是常量
名称
- 采用描述性名称
- 名称应该与抽象层级相符
- 尽可能使用标准命名法
- 无歧义的名称
- 作用范围较大的名称选用长名称(范围小的可以用短名称,比如循环变量i)
- 避免编码式命名(比如匈牙利语标记法等)
- 名称应该说明副作用(比如createOrReturnOos优于getOos)
测试
- 测试不足
- 使用覆盖率工具包
- 别略过小测试
- 被忽略的测试就是对不确定事物的疑问
- 测试边界条件
- 全面测试相近的缺陷
- 测试失败的模式有启发性
- 测试覆盖率的模式有启发性
- 测试应该快速
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 duval1024@gmail.com