代码设计
AI WARNING
未进行对照审核。
代码设计的目标是编写高质量的代码,其核心在于提升代码的可读性、可维护性和可靠性。良好的代码设计不仅方便团队协作,减少软件维护成本,还能构建稳健的系统。
设计易读的代码
易读的代码如同清晰的文章,能够让他人(以及未来的自己)快速理解其意图和逻辑。这对于软件维护和团队协作至关重要。
代码规范
统一的代码规范是提高代码可读性的基础,常见的规范包括格式、命名和注释。
布局格式
良好的布局能够清晰地展现代码的逻辑结构。
- 
使用缩进与对齐表达逻辑结构
- 一致的缩进(通常为 4 个空格)能够清晰地反映代码块的层次。
 - 对齐相似的代码行可以增强可读性。
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 示例:良好的缩进和对齐
public class Sales extends DomainObject {
// ...
public double getTotal() {
Iterator iter = salesLineItemMap.entrySet().iterator();
while (iter.hasNext()) { // 缩进体现循环体
Map.Entry entry = (Map.Entry) iter.next();
Object val = entry.getValue();
total += ((SalesLineItem)val).getSubTotal();
}
return total;
}
public void addSalesLineItem(long commodityID, long quantity) {
SalesLineItem item = new SalesLineItem(commodityID, quantity); // 对齐
salesLineItemMap.put(commodityID, item); // 对齐
}
} - 
将相关逻辑组织在一起
- 在类定义中,相关的成员(如属性、构造方法、公有方法、保护方法、私有方法)应该组织在一起,形成逻辑分组。
 - 例如,可以将所有成员变量声明放在类的顶部,接着是构造方法,然后是不同访问级别的方法。
 
 - 
使用空行分割逻辑
- 在不同的逻辑单元之间使用空行,可以像段落一样分隔代码,使得结构更清晰。
 - 例如,在 
switch语句的各个case块之间,或在不同功能的方法调用序列之间。 
 - 
语句分行
- 避免将过长的语句放在一行,这会影响可读性,尤其是在屏幕或打印输出时。
 - 复杂的条件表达式或方法调用链可以适当换行并对齐。
 
1
2
3
4
5
6// 示例:长语句断行
if ( (this.obj instanceof MyClass) &&
(this.field == obj.field) &&
(isConditionMet()) ) {
// ...
} 
命名
有意义的命名是代码自解释能力的关键。
- 使用有意义的名称:变量名、函数名、类名应准确反映其用途或代表的实体。例如,表示销售信息的类命名为 
Sales而不是ClassA。 - 遵循命名约定:
- 类、接口:名词或名词短语,每个单词首字母大写(PascalCase),如 
SalesOrder。 - 方法/函数:动词或动宾短语,第一个单词小写,后续单词首字母大写(camelCase),如 
calculateTotalPrice()。 - 变量:名词或名词短语,同方法名(camelCase),如 
customerName。 - 常量:全大写,单词间用下划线分隔,如 
MAX_CONNECTIONS。 
 - 类、接口:名词或名词短语,每个单词首字母大写(PascalCase),如 
 - 名称与实际内容相符:例如,一个不仅计算变化还维护账单数据的类,用 
Payment或Bill比ChangeCalculator更合适。 - 临时变量命名:对于生命周期极短的临时变量(如 
for循环计数器),可以使用i,j,k等;但若有特定含义,仍应赋予有意义的名称。 - 避免使用易混淆的字符:如 
l(小写 L)和1(数字一),O(大写 O)和0(数字零)。 - 避免使用无逻辑的缩写:如 
wrttn代表written,这会降低可读性。 
注释
注释是对代码的补充说明,帮助理解代码功能、设计思路和使用方法。
- 
注释类型(以 Java 为例)
- 语句注释:
// ...用于单行注释。 - 标准注释:
/* ... */用于多行注释。 - 文档注释:
/** ... */用于生成 API 文档(如 Javadoc)。 
 - 语句注释:
 - 
文档注释
- 目的:为包、类、接口、方法、字段等提供规范化的说明,以便工具(如 Javadoc)提取生成 API 文档。
 - 内容:
- 包:总结和概述。
 - 类/接口:描述其功能、用途。
 - 方法:描述其功能、参数、返回值、可能抛出的异常。
 - 字段:描述重要字段的含义、用法和约束。
 
 - 常用 Javadoc 标签:
- 类/接口级别:
@author(作者名)、@version(版本号)、@see(相关引用)、@since(最早使用的 JDK 版本)、@deprecated(不推荐使用的警告)。 - 方法级别:
@param(参数及其意义)、@return(返回值)、@throws(异常类及抛出条件)、@see、@since、@deprecated。 
 - 类/接口级别:
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22/**
* LoginController 负责处理来自 LoginDialog 的用户登录请求。
* 它验证用户凭据并将结果反馈给界面。
* @author Your Name
* @version 1.0
* @see presentation.LoginDialog
* @since 2023-01-01
*/
public class LoginController {
/**
* 验证用户登录。
* @param id 用户 ID,由界面传入。
* @param password 用户密码,由界面传入。
* @return 如果验证成功返回 true,否则返回 false。
* @throws DBException 如果数据库连接失败。
* @see businesslogic.domain.User
*/
public boolean login(long id, String password) throws DBException {
User user = new User(id);
return user.login(password);
}
} - 
内部注释
- 要有意义:解释代码「为什么」这么做,而不是「做什么」(代码本身应清晰表达「做什么」)。避免简单重复代码的含义。
 - 重视对数据类型的注释:解释复杂数据结构、关键变量的含义和取值范围。
 - 重视对复杂控制结构的注释:解释复杂算法、决策逻辑的思路。
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14// 示例:内部注释
// 为了快速存取,使用 HashMap 组织销售商品列表
// Key 是商品 ID,取值范围是 1...MAX_ID
HashMap<Long, SalesLineItem> salesLineItemMap;
// ...
// 更新 Member 信息(此注释意义不大,代码已清晰)
member.update();
// 遍历销售商品项,更新每个商品的状态
while (iter.hasNext()) {
// ...
} 
设计易维护的代码
易维护的代码意味着当需求变更或缺陷修复时,可以轻松、安全地修改代码,而不会引入新的问题。
- 
小型任务(高内聚)
- 每个函数或方法应专注于完成一个单一、明确的任务(高内聚)。
 - 如果一个内聚的任务本身逻辑简单、代码量少,则其可维护性较好。
 - 如果一个内聚的任务依然复杂、代码冗长,应将其进一步分解为多个高内聚、低耦合的小型任务。
 
 - 
处理复杂决策
复杂的条件判断是维护的难点。- 使用新的布尔变量简化:将复杂的布尔表达式赋值给一个有意义名称的布尔变量。  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 不推荐
if ((atEndOfStream) && (error != inputError) &&
(MIN_LINES <= lineCount) && (lineCount <= MAX_LINES) &&
(!errorProcessing(error))) {
// ...
}
// 推荐
boolean allDataRead = (atEndOfStream) && (error != inputError);
boolean validLineCount = (MIN_LINES <= lineCount) && (lineCount <= MAX_LINES);
boolean noProcessingError = !errorProcessing(error);
if (allDataRead && validLineCount && noProcessingError) {
// ...
} - 使用有意义的名称封装复杂决策:将复杂的判断逻辑封装到一个独立的、名称清晰的方法中。  
1
2
3
4
5
6
7
8
9// 不推荐
if ((id > 0) && (id <= MAX_ID)) { /* ... */ }
// 推荐
if (isIdValid(id)) { /* ... */ }
// ...
private boolean isIdValid(int id) {
return (id > 0) && (id <= MAX_ID);
} - 表驱动编程:对于多重条件判断(尤其是 
if-else if-else或switch结构),可以使用表(如数组、Map)来存储条件和对应的动作/结果,通过查表代替复杂的逻辑判断。这使得逻辑更清晰,且易于修改和扩展。 
 - 使用新的布尔变量简化:将复杂的布尔表达式赋值给一个有意义名称的布尔变量。  
 
表驱动示例:根据积分赠送礼品等级
假设积分规则如下:
- 积分 1000:无礼品
 - 1000 积分 2000:礼品等级 1
 - 2000 积分 5000:礼品等级 2
 - 积分 5000:礼品等级 3
 
传统 if-else:
1  | int getGiftLevel(int points) {  | 
表驱动实现:
1  | // 定义阈值和对应的礼品等级  | 
表驱动的核心思想是将逻辑判断转化为数据查询,当规则频繁变动或规则数量很多时,维护数据表通常比维护复杂的
if-else语句更容易。
- 
数据使用
- 专变量专用:不要将变量用于与其命名不符的目的(如用 
total临时做循环计数器)。 - 单一用途:一个变量只用于一个目的。避免在代码不同部分让同一变量表示不同含义。
 - 限制全局变量:全局变量会增加模块间的耦合,难以追踪修改。如果必须使用,要明确注释其声明和使用处。
 - 避免魔法数值/字符串:将代码中直接出现的、含义不明的数值或字符串(如 
15代表天数,"MALE"代表性别)定义为有意义名称的常量。 
 - 专变量专用:不要将变量用于与其命名不符的目的(如用 
 - 
明确依赖关系
- 类之间、模块之间的依赖关系应清晰明了。模糊的依赖关系(如通过全局变量间接交互)会使代码难以理解和修改,容易产生未预期的连锁反应。
 - 使用 Javadoc 的 
@see或其他文档方式明确依赖。 
 
设计可靠的代码
可靠的代码能在各种情况下正确执行,或在发生错误时能以可控的方式处理。
- 
契约式设计(Design by Contract, DbC)
- 核心思想:软件模块(如类、方法)之间的交互建立在明确的「契约」之上。契约规定了调用方和被调用方的责任和权利。
 - 组成:
- 前置条件(Precondition):调用方必须满足的条件,才能调用该方法。
 - 后置条件(Postcondition):方法完成后,被调用方保证满足的条件。
 - 不变式(Invariant):在方法执行期间(对于类而言,是对象生命周期中稳定状态时)必须保持为真的条件。
 
 - 实现方式:
- 异常方式:在方法入口检查前置条件,若不满足则抛出异常。在方法出口(或 
return前)检查后置条件,若不满足也抛出异常。1
2
3
4
5
6
7
8
9
10
11
12
13public double getChange(double payment, double total) throws PreException, PostException {
// 前置条件检查
if (payment <= 0 || payment < total) {
throw new PreException("Sales.getChange: Payment " + String.valueOf(payment) +
", Total " + String.valueOf(total));
}
double result = payment - total;
// 后置条件检查(示例,实际可能更复杂)
if (result < 0) { // 假设找零不能为负
throw new PostException("Sales.getChange: Result " + String.valueOf(result));
}
return result;
} - 断言方式:使用断言(Assertion)来检查前置条件和后置条件。
- Java 断言语句:
assert Expression1 (: Expression2);Expression1:一个布尔表达式。Expression2(可选):一个值,如果Expression1为false,Expression2的字符串形式会作为AssertionError的一部分。- 如果 
Expression1为true,断言不影响程序执行。 - 如果 
Expression1为false,抛出AssertionError。 
 - 断言通常用于开发和测试阶段,默认情况下在运行时可能被禁用以提高性能。
 
1
2
3
4
5
6
7
8
9
10
11
12public double getChange(double payment, double total) { // throws AssertionError (隐式)
// 前置条件检查
assert (payment > 0 && payment >= total) :
"Sales.getChange: Payment " + String.valueOf(payment) +
", Total " + String.valueOf(total);
double result = payment - total;
// 后置条件检查
assert (result >= 0) : "Sales.getChange: Result " + String.valueOf(result);
return result;
} - Java 断言语句:
 - 比较:异常是方法契约的一部分,调用者应准备处理。断言主要用于内部一致性检查,通常不期望调用者捕获 
AssertionError。 
 - 异常方式:在方法入口检查前置条件,若不满足则抛出异常。在方法出口(或 
 
 - 
防御式编程
- 核心思想:方法在与外部环境(其他方法、操作系统、用户输入等)交互时,不能完全信任外部的正确性。要在发生错误时保护方法内部不受损害。
 - 常见场景:
- 输入参数是否合法?
 - 用户输入是否有效?
 - 外部文件是否存在/可读/可写?
 - 对象引用是否为 
NULL? - 其他对象是否已正确初始化/执行了必要方法?
 - 数据库/网络连接是否正常?
 
 - 异常和断言都可以用于实现防御式编程。
 
 
使用模型辅助设计复杂代码
对于复杂的逻辑,使用模型可以帮助梳理思路,减少错误。
- 
决策表
- 
一种表格化的方式,用于表示复杂的条件组合及其对应的动作。
 - 
结构:
- 条件声明:列出所有相关的条件。
 - 行动声明:列出所有可能的行动。
 - 条件选项:针对每条规则,标出条件的取值(如 True/False, Y/N, 具体值范围)。
 - 行动选项:针对每条规则,标出应执行的行动(如 X 标记)。
 
 - 
优点:清晰、无歧义,易于检查完备性和一致性。
条件/行动 规则 1 规则 2 规则 3 条件声明 - - - prePoint < 1000 是 否 否 postPoint >= 1000 - 是 否 prePoint < 2000 - 是 否 postPoint >= 2000 - - 是 … 行动声明 - - - Gift Event Level 1 X - - Gift Event Level 2 - X - Gift Event Level 3 - - X  
 - 
 - 
伪代码
- 介于自然语言和编程语言之间的一种描述算法的方式。
 - 使用编程语言的关键字(如 
IF-THEN-ELSE,WHILE,FOR)和结构,但允许使用自然语言描述具体操作。 - 优点:专注于逻辑流程,忽略具体语法细节,便于沟通和快速原型设计。
 
1
2
3
4
5
6
7
8
9得到 SalesLineItem 的迭代器
WHILE 迭代器有下一个元素
找到 SalesLineItem 对象
按照更新步骤进行更新:
得到相应的 Mapper
将自己的信息转为层间传递的 PO 对象
将 PO 对象交给 Mapper
Mapper 完成更新
END WHILE - 
程序流程图
- 使用标准化的图形符号表示程序的控制流和操作步骤。
 - 常用符号:圆角矩形(开始/结束)、矩形(处理步骤)、菱形(判断)、平行四边形(输入/输出)。
 - 优点:直观展示复杂逻辑路径。
 
graph TD A[开始] --> B{读入 N}; B --> C[M=1, F=1]; C --> D{M=N?}; D -- 是 --> E[打印 F]; E --> F[结束]; D -- 否 --> G[F = F * M]; G --> H[M = M + 1]; H --> D; 
为代码开发单元测试用例
单元测试是验证代码模块(通常是方法或类)功能正确性的重要手段。
- 
为方法开发测试用例
- 基于规格的测试(黑盒):不关心内部实现,只根据方法的功能说明(规格)设计测试用例。
- 等价类划分:将输入数据划分为若干个等价类,从每个类中选取代表性数据作为测试用例。
 - 边界值分析:针对等价类的边界以及略过边界的值设计测试用例,这些地方容易出错。
 
 - 基于代码的测试(白盒):根据代码的内部逻辑结构设计测试用例,以达到一定的代码覆盖率。
- 语句覆盖:每个可执行语句至少执行一次。
 - 分支覆盖(判定覆盖):每个判断条件的真假分支至少执行一次。
 - 路径覆盖:程序中所有可能的执行路径至少执行一次(通常难以完全实现)。
 
 - Mock 对象:当被测方法依赖于其他复杂对象或外部系统时,可以使用 Mock 对象(模拟对象)来替代这些依赖,使得测试环境更可控、测试执行更快。例如,测试 
Sales.total()方法时,它依赖SalesLineItem.subTotal(),可以创建一个MockSalesLineItem类来提供固定的subTotal()返回值。 
 - 基于规格的测试(黑盒):不关心内部实现,只根据方法的功能说明(规格)设计测试用例。
 - 
为类开发测试用例
- 复杂类通常有多种状态,方法的执行会改变类的状态,并可能影响其他方法的行为。
 - 除了测试类的每个独立方法外,还需要测试不同方法之间因状态改变而产生的交互影响。
 - 状态图可以帮助分析类的状态转换,并据此设计测试序列(方法调用顺序)来覆盖不同的状态和转换。
 
 
代码复杂度度量
程序复杂度是导致编程困难和维护成本高的主要原因之一。
圈复杂度(Cyclomatic Complexity) - McCabe
- 基本思路:衡量程序中独立路径的最大数量。
 - 计算方法:
- 基于流程图:
- 其中, 是程序的控制流程图, 是图中边的数量, 是图中节点的数量。
 
 - 基于决策点:
-  是程序中决策点(如 
if,while,for中的条件,case语句等)的数量。每个case分支算一个决策点。 
 
 - 基于流程图:
 - 度量的意义(McConnell 建议):
- 0-5:子程序可能还不错。
 - 6-10:需要考虑简化子程序。
 - 10+:应将子程序的某部分拆分成独立的子程序并调用它。
 - 这只是一个警示,说明子程序可能需要重新设计。
 
 - 类的复杂度:可以定义为类中所有方法代码复杂度的总和。
 
问题代码警示
借鉴《代码大全》
以下是一些常见的导致代码质量下降的做法,应尽量避免:
- 
变量处理不当
- 定义与初始化:
- 避免隐式声明(某些语言特性)。
 - 声明所有变量,并在靠近首次使用的地方初始化。
 - 尽可能使用 
final(Java)或const(C++)声明不应改变的变量。 
 - 作用域:
- 使变量作用域尽可能小(局部化)。
 - 直到即将使用时再赋值。
 
 - 持续性:
- 编写代码时假设数据没有持续性,除非显式处理。
 - 养成在使用所有数据前声明和初始化的习惯。
 
 - 单一用途:一个变量只用于一个明确的目的,避免复用导致含义混淆。
 - 避免隐含意义:如用 
pageCount = -1表示错误,应使用更明确的方式(如异常或专门的状态变量)。 
 - 定义与初始化:
 - 
数值处理问题
- 避免「神秘数值」:使用有意义的常量名代替硬编码的数字。
 - 整数:注意整数除法(截断)、整数溢出(尤其在中间计算步骤)。
 - 浮点数:
- 避免数量级相差巨大的数之间的加减运算(精度损失)。
 - 避免直接进行等量判断(
float_a == float_b),应使用一个小的容差范围(Math.abs(float_a - float_b) < EPSILON)。 - 注意舍入误差。
 
 
 - 
子程序(方法/函数)设计
- 创建理由:降低复杂度、引入抽象、避免代码重复、支持子类化、隐藏实现细节(顺序、指针操作)、提高可移植性、简化复杂布尔判断、改善性能(有时通过集中优化)。
 - 良好命名:
- 准确描述子程序所做的所有事情。
 - 避免无意义、模糊的动词(如 
handleData,processInput)。 - 函数命名应能体现返回值(如 
isReady(),getColor())。 - 过程命名使用「动词+宾语」形式(如 
printDocument())。 
 
 - 
一般控制问题
- 布尔表达式:
- 用 
true和false做布尔判断,而不是0和1(除非语言特性如此)。 - 简化复杂表达式(引入布尔变量或布尔函数)。
 - 使用肯定形式的布尔表达式(
if (statusOk)通常比if (!statusNotOk)好)。 - 使用括号明确优先级。
 
 - 用 
 - 复合语句:始终使用花括号 
{}包围if,else,while,for的执行体,即使只有一条语句,以避免悬挂else等问题。 - 空语句:如果循环体为空(如 
while(processNext() != DONE);),应使其明显,例如:while(processNext() != DONE) { /* 空循环体 */ }或while(processNext() != DONE) continue;。 - 驯服深层嵌套:过深的嵌套难以理解和维护。可以通过以下方式简化:
- 将部分条件检测提取到前面。
 - 使用 
break或return提前退出。 - 将嵌套 
if转换为if-then-else if序列或switch/case。 - 将深层嵌套的代码抽取到独立的子程序中。
 
 
 - 布尔表达式:
 - 
如何编写难以维护的代码(反面教材 - Green1997)
- 在注释中「说谎」,或不让代码和注释同步。
 - 到处使用无意义的注释,如 
/* add 1 to i */,从不注释整体意图。 - 让每个方法都比它的名字多做点事(副作用)。
 - 大量使用首字母缩写命名。
 - 以效率为名,避免封装,暴露内部实现。
 - 修改一处功能,需要改动 N 个地方,并且不记录这些地方。
 - 大量使用复制/粘贴/克隆/修改。
 - 从不对变量注释其用法、边界、有效值等。
 - 一行中写尽可能多的代码。
 - 不要使用任何代码格式整理工具,手动制造错误的对齐。
 - 绝不使用 
{}界定if/else的代码块,尤其在嵌套时。 - 只要生命周期许可,就重用那些无关的变量。
 - 从不处理异常,名义上是因为「好的代码不会失败」。
 - 到处硬编码常量值,如数组大小 
100。 - 保留过期的、不再使用的变量或方法,并加上令人困惑的注释。
 - 把所有成员方法和变量都声明为 
public。