这份文档不绑定任何具体项目或语言。目标是:让你写出“别人一眼就懂”的代码。
这份文档只专注于代码可读性、可维护性
本文内容为方法论参考,实践中请根据项目实际情况权衡使用,避免完全的机械套用。
你将收获什么
- 一套简单好记的写码习惯与检查清单
- 大量“前后对比”的示例(看得懂、照着用)
- 一条从“草稿”到“整洁”的重构路径
- 想先看效果请直接翻到第七节
一、总目标(为什么要整洁)
整洁代码的标准很朴素:
- 别人看到后,10 秒内能回答两个问题:
- 这段代码在做什么? 2) 为什么要这样写?
- 读者能快速定位“哪里需要改”“怎么改不会出错”。
整洁 ≠ 花哨的排版。整洁 = 清晰的表达 + 合理的结构 + 可预期的行为。
给自己三条底线:
- 不让读者猜(用清晰的名字和结构表达意图)
- 不给读者惊喜(函数名和行为一致,没有隐藏副作用)
- 不留坑给读者(不返回/传递 null,不埋魔法数,错误有清晰提示)
二、命名(让名字说人话)
核心思路:名字要“名副其实”,让人一眼看出意图。
2.1 基本规则
- 用能读出来的名字:避免
genymdhms,改成generateTimestamp。 - 不要把类型写进名字:
nameString,String 多余,类型由语言/IDE 知道。 - 避免空话:
Data/Info/Manager/Util意义不清。更具体一些,比如Snapshot、Calculator、Authorizer。 - 类/对象名用名词或名词短语:
Order、AddressBook、PricePolicy。 - 方法名用动词或动词短语:
calculateTotal、loadConfig、saveUser。 - 布尔返回的方法建议以
is/has/should/can开头:isEligible、shouldRetry。 - 别害怕长名字:清晰优先于短小,能一眼看懂最重要(例如
calculateMonthlyRepayment)。 - 按业务起名:尽量使用领域内大家都懂的词(统一语言),减少歧义。
2.2 可搜索性
- 避免单字母变量和“魔法数字”。把数字提成有名常量:
MAX_RETRY = 3。 - 项目里出现过的同一概念,尽量使用同一个词,不要时而
customer时而client。
2.3 加语境(上下文)
- 零散字段可加前缀:
addrStreet、addrCity。 - 更好的做法:引入值对象
Address,字段内部就用street/city/zip。
2.4 名字长度与作用域
- 作用域越大,名字越要清晰(可能越长)。循环里的
i/j可以,跨方法的变量就不行。
2.5 不要掩盖副作用
- 如果方法会创建对象,就别叫
getX();请叫createOrGetX(),把副作用说清楚。
2.6 集合与单位
- 集合用复数:
users、orders。 - 明确单位:
timeoutMs、intervalSeconds、amountCents。 - 避免容器后缀:优先用语义表达集合(
accounts而非accountList)。 - 不可数名词的集合(如 equipment、staff):使用“可数的分类词”来命名,如
equipmentItems、staffMembers,或使用更具体的集合名词inventory、personnel。 - 去掉冗余词:
account不要写成accountData(除非确实存在“非数据”的同名概念)。
2.7 前后对比
// 之前(看不出意图)
String data; boolean flag; List<ProductData> list;
// 之后(意图清晰)
String applicantReason;
boolean shouldRetry;
List<ProductSnapshot> products;
三、函数(方法):短、小、只做一件事
你可以把函数想象成文章中的“段落”,每个段落只讲一个重点。
3.1 只做一件事
- 同一个函数里,不要同时“校验 + 计算 + 保存 + 通知”。
- 顶层函数像“目录”,列出步骤;细节放到子函数里。
- 使用描述性的名称:别害怕长名字,函数名要能说清“这一小段在做什么”。
// 顶层像目录
public void applyRemission(Request request) {
validate(request);
Assessment assessment = assess(request);
Plan plan = makePlan(assessment);
save(request, plan);
notifyStakeholders(plan);
}
3.2 参数越少越好
- 超过 3 个参数,就考虑把参数封装成一个对象(参数对象/值对象/Builder)。
// 参数对象示例(Java)
class PricingInput {
BigDecimal subtotal;
BigDecimal discount;
BigDecimal taxRate;
}
PricingResult calculate(PricingInput input) { ... }
3.3 少用布尔参数
save(user, true)这类写法,说明函数在做两件事。改为两个函数:createUser(user)/updateUser(user)。
3.4 命令与查询分离
- “查询”只读不改:像问问题;“命令”改状态:像发指令。
- 好处:读的人一眼就知道“这句会不会改东西”。
3.5 用“早返回”降低嵌套
def process(user):
if not user:
return "no user"
if not user.active:
return "inactive"
return do_real_work(user)
3.6 把复杂条件变成“会说话”的函数
// 之前
if (timer.hasExpired() && timer.isRecurrent() && !user.isSuspended()) {
delete(timer);
}
// 之后
if (shouldDeleteTimer(timer, user)) {
delete(timer);
}
private boolean shouldDeleteTimer(Timer timer, User user) {
return timer.hasExpired() && timer.isRecurrent() && !user.isSuspended();
}
3.7 顺序自上而下
- 调用者在上,被调用者在下;读代码时,像顺着目录往下翻。
3.8 业务不相干的代码不要为了“复用”而硬复用
- 复用的前提是“同一个语义/同一个目的/同一个业务”。不相关就不要强行共用一个函数。
3.9 参数是“输入”,不要当“输出”用
- 不要在函数内部悄悄修改传入的参数对象;如果需要返回结果,直接返回新值或新对象。
3.10 只和“朋友”说话
- 一个对象尽量只调用自己拥有的对象的方法,避免跨越多层(避免 A 调 B 的 C 的 D),降低耦合和理解成本。
3.11 行与行之间尽量解耦
- 避免上一行的输出成为下一行的“唯一入口”。必要时引入中间变量或提炼小函数,让每一步都“有名字”。
示例 1(一步套一步,难插入校验/日志):
// 之前:每一步都紧挨着下一步,难以在中间加检查/日志
String result = renderHtml(transform(enrich(parse(requestBody))));
// 之后:给每步“起名字”,更好读、好调试、好插入校验
Payload payload = parse(requestBody);
Payload enriched = enrich(payload);
Payload transformed = transform(enriched);
String result = renderHtml(transformed);
示例 3(SQL/查询构建):
```python
# 之前:
rows = db.query("SELECT ... WHERE a=? AND b IN (?) ORDER BY c LIMIT 10", a, bs)
# 之后:
sql = "SELECT ... WHERE a=? AND b IN (?) ORDER BY c LIMIT 10"
params = (a, bs)
rows = db.query(sql, params)
四、注释:能不用就不用,用就写“意图”
注释不是补救糟糕代码的工具。优先用“好名字 + 好结构”来表达。
4.1 注释的缺陷
- 时效性:代码逻辑变更,但注释忘记修改,导致注释解释错误
- 无辅助检查:代码有编译和编辑器检查,但注释错误没有
- 解铃还需系铃人:一条错误的注释如果作者不去删除,别人不敢删那将一直存在下去
4.2 什么时候写
- 类/方法头:说明做什么、输入输出、前置条件、可能的副作用。
- 复杂算法:写清“思路”和“来源链接”。
- 法律/合规:版本、条款、限制。
- 决策意图:为什么选择这个方案,有什么权衡。
- 警示后果:风险、性能、边界。
4.3 什么时候不要写
- 不要把代码注释掉(删掉它,Git 会保存历史)。
- 不要把代码复述一遍(“x++ 表示加一”这种没必要)。
4.4 TODO 的正确用法
- TODO 是“明确的待办”:写具体行动、原因、最好加日期或责任人。
- IDEA支持高亮提示搜索
// TODO(2025-01, Alice): 支持多币种结算,参考支付网关 V2 文档
五、格式:一致、简洁、读起来顺
5.1 空行有“语义”
- 尽量少加空行;如果想靠空行分段,优先考虑把这段逻辑提炼为一个方法。空行只用于极少数必要的语义分隔。
5.2 长参数换行
- 每一行尽量表达一个“自然的片段”,便于眼睛扫描。
5.3 文件内成员顺序(建议)
- 常量 → 字段 → 构造器 → public 方法 → protected → private → 内部类。
5.4 一致性
- 一个仓库里,缩进、括号风格、导入顺序尽量统一(IDE 自动格式化即可)。
六、错误处理:把“异常路径”从“主路径”里拿出来
6.1 用异常代替返回码
- 让主逻辑更干净。把异常“集中在边界处”处理:边界指系统的入口/出口层,如控制器(Controller)、拦截器/过滤器(Interceptor/Filter)、接口适配器(Adapter)、任务调度器(Job)、消息消费器(Consumer)。这些地方统一把异常翻译为对外的返回或日志,而业务服务内部不到处写 try/catch。
public Response handle(Request req) {
try {
return service.execute(req);
} catch (BusinessException ex) {
return fail("BUSINESS_ERROR", ex.getMessage());
}
}
6.2 不要随便抛“受检异常”(需要强制 try/catch 的那种)
- 它会把捕获压力一路传上去。更推荐:自定义“业务异常”(非受检),在适当的边界统一处理。
6.3 根据调用方来设计和命名异常
- 调用方需要知道“出了什么错、如何恢复”。异常名和信息要帮到他。
6.4 别返回/传递 null
- 返回空集合而不是 null:
Collections.emptyList()。 - 可选值用
Optional(用于返回值,不建议用在字段上)。
6.5 错误处理就是“一件事”
- 函数应该只做一件事。错误处理也是一件独立的事:专门的错误处理函数以 try 开头、以 catch/finally 结束,且之后不再掺杂其他业务逻辑。
// 专职错误处理,不再在后面继续业务流程
public Response tryExecute(Request req) {
try {
return service.execute(req);
} catch (DomainException ex) {
return fail("BUSINESS_ERROR", ex.getMessage());
} finally {
audit(req);
}
}
6.6 抽离 try/catch,集中处理
- try/catch 容易让主流程变得凌乱。把它们抽到“一个位置”统一处理(边界层),例如全局异常处理器;业务服务里保持直观的主路径。
// 例:全局异常处理(示意)
@ControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(DomainException.class)
ResponseEntity<?> handleDomain(DomainException ex) {
return ResponseEntity.badRequest().body(Map.of("code", "BUSINESS_ERROR", "msg", ex.getMessage()));
}
}
七、像讲故事一样写代码(从草稿到整洁)
7.1 写之前先“讲一遍”
- 用自己的话把需求讲通顺。如果讲不顺,代码也会乱。
7.2 先写草稿(别急着美化)
- 把想到的步骤都写出来,让“故事完整”。
// 草稿:内容可能又长又乱,但先把路走通
void registerUser(Input in) {
// 校验
// 查重
// 入库
// 发欢迎消息
}
7.3 开始重构(像分段落)
- 从最上层开始,拆成独立小函数;每个函数名是一句“这段在做什么”的话。
void registerUser(Input in) {
validate(in);
ensureNotExists(in);
long userId = saveUser(in);
sendWelcome(userId);
}
7.4 递归下钻,直到“每个函数只做一件事”
- 看到函数变长了,就继续拆。直到读起来像在“顺着故事走”。
7.5 一个更典型的详细示例(从复杂草稿到精简编排)
目标:实现“下单并发起支付”流程:校验 → 规范化地址 → 用户状态检查 → 库存/优惠券校验 → 价格计算(小数精度、税费、运费、汇率)→ 幂等检查 → 事务内落库与扣减库存 → 生成支付意图 → 事务提交 → 发送事件与通知。
步骤 0:先写一个会失败的小测试(或最小脚本)说明期望:
- 下单成功返回
orderId;库存不足抛特定错误;幂等键重复返回相同orderId。
步骤 1:用自然语言讲通顺(白板/注释)
- 数量必须 > 0;用户未被拉黑;地址需要规范化(去空格、标准省市区)。
- 有优惠券则校验有效期、适用范围;库存不足立即失败。
- 价格采用统一的 Money 封装与舍入方式;含税、含运费。
- 同一幂等键只创建一次订单;创建失败要回滚库存。
- 订单写库与库存扣减放在同一事务;支付意图创建后再发通知。
步骤 2:复杂草稿(混杂很多细节,读起来吃力,但先跑通)
OrderId placeOrder(OrderRequest req) {
// 1) 请求校验(validateRequest)
if (req == null) throw new IllegalArgumentException("req");
if (req.qty <= 0) throw new IllegalArgumentException("qty");
// 2) 地址规范化(normalizeAddress)
String province = req.addrProvince == null ? null : req.addrProvince.trim();
String city = req.addrCity == null ? null : req.addrCity.trim();
// 3) 用户状态校验(verifyUserStatus)
if (userRepo.isBlacklisted(req.userId)) throw new ForbiddenException();
// 4) 优惠券校验(verifyCouponIfPresent)
if (req.couponId != null) {
Coupon c = couponRepo.find(req.couponId);
if (c == null || c.expired() || !c.applicableTo(req.sku)) throw new InvalidCoupon();
}
// 5) 库存校验(ensureInStock)
if (!stockRepo.has(req.sku, req.qty)) throw new OutOfStockException();
// 6) 价格计算(computeTotalPrice)
BigDecimal subtotal = req.unitPrice.multiply(BigDecimal.valueOf(req.qty));
BigDecimal discount = BigDecimal.ZERO;
if (req.couponId != null) {
discount = couponService.calcDiscount(req.couponId, subtotal);
}
BigDecimal shipping = freightService.quote(province, city, req.sku, req.qty);
BigDecimal tax = taxService.taxOf(subtotal.subtract(discount));
BigDecimal total = subtotal.subtract(discount).add(shipping).add(tax);
total = total.setScale(2, RoundingMode.HALF_UP);
// 7) 幂等检查(ensureIdempotent)
if (idempotencyRepo.exists(req.idempotencyKey)) {
return idempotencyRepo.getOrderId(req.idempotencyKey);
}
OrderId orderId = null;
// 8) 事务内创建(createOrderTransactionally)
try {
tx.begin();
stockRepo.deduct(req.sku, req.qty);
orderId = orderRepo.insert(req.userId, req.sku, req.qty, total, province, city);
paymentClient.createIntent(orderId, total);
idempotencyRepo.save(req.idempotencyKey, orderId);
tx.commit();
} catch (RuntimeException e) {
tx.rollback();
throw e;
}
// 9) 事件与通知(publishDomainEvents/notifyUser)
eventBus.publish(new OrderCreated(orderId));
notifier.notifyCreated(orderId);
return orderId;
}
步骤 3:精简明确的“目录化编排”(顶层只保留步骤,像读目录)
OrderId placeOrder(OrderRequest req) {
validateRequest(req);
normalizeAddress(req);
verifyUserStatus(req);
verifyCouponIfPresent(req);
ensureInStock(req);
Money total = computeTotalPrice(req);
return ensureIdempotent(req, () -> createOrderTransactionally(req, total));
}
步骤 4:提炼参数对象/上下文与值对象,避免长参数链、统一单位与精度
final class Money { /* 统一货币、scale 与舍入,禁止直接用 BigDecimal 裸值 */ }
final class Address { final String province; final String city; }
final class PricingInput { final Money unitPrice; final int qty; final Money shipping; final Money discount; final Money tax; }
final class OrderContext { final long userId; final String sku; final int qty; final Address address; final String idempotencyKey; }
Money computeTotalPrice(OrderRequest req) {
OrderContext ctx = mapper.toContext(req);
PricingInput in = pricingAssembler.assemble(ctx);
return pricingService.calculate(in);
}
步骤 5:事务与边界异常处理(主流程不散落 try/catch)
// 事务边界集中到创建过程
@Transactional
OrderId createOrderTransactionally(OrderRequest req, Money total) {
reserveInventory(req);
OrderId id = persistOrder(req, total);
createPaymentIntent(id, total);
recordIdempotency(req.idempotencyKey, id);
publishDomainEvents(id);
notifyUser(id);
return id;
}
// 控制器/适配器层统一异常到响应
@PostMapping("/orders")
ResponseEntity<?> create(@RequestBody OrderRequest req) {
try {
return ResponseEntity.ok(service.placeOrder(req));
} catch (OutOfStockException ex) {
return ResponseEntity.status(409).body(Map.of("code","OUT_OF_STOCK"));
} catch (InvalidCoupon ex) {
return ResponseEntity.badRequest().body(Map.of("code","INVALID_COUPON"));
}
}
步骤 6:幂等性策略(可拓展为去重表/唯一索引)
OrderId ensureIdempotent(OrderRequest req, Supplier<OrderId> creator) {
return idempotencyRepo.find(req.idempotencyKey).orElseGet(() -> {
OrderId id = creator.get();
idempotencyRepo.save(req.idempotencyKey, id);
return id;
});
}
九、写完之前的“检查清单”(过一遍就更稳)
命名
- 名字是否名副其实、读得出来、搜得到?
- 集合用复数?单位说清楚了吗?
- 有无隐藏副作用的命名(比如
get却会创建)?
函数
- 每个函数只做一件事吗?
- 参数 ≤ 3 吗?有布尔参数吗(能不能拆成两个函数)?
- 有没有早返回、把复杂条件变成会说话的函数?
注释
- 有无“意图型”注释(为什么这样做)?
- 有没有遗留的“被注释掉的代码”(应该删掉)?
错误处理
- 用异常而不是返回码处理错误了吗?
- 是否避免了返回/传递 null?
格式
- 空行是否帮助阅读,而不是随意分割?
- 顺序是否“从上到下”自然阅读?
测试
- 关键路径和异常路径有测试吗?
- 一个测试是否只验证一个结论?
十、前后对比(快速感受提升)
10.1 重命名 + 拆函数
// 之前:
function handle(a, b, c, flag) {
if (!a) return -1;
// ... 很多混杂逻辑 ...
}
// 之后:
function createOrder(request) {
validate(request);
const price = calculatePrice(request);
const orderId = saveOrder(request, price);
notifyCreated(orderId);
return orderId;
}
10.2 复杂判断语义化
# 之前
if age > 65 and score >= 80 and not has_late_payment:
approve()
# 之后
if is_vip_customer(age, score, has_late_payment):
approve()
def is_vip_customer(age, score, has_late_payment):
return age > 65 and score >= 80 and not has_late_payment
10.3 布尔参数拆分
// 之前:
saveUser(user, true); // true 表示创建,false 表示更新
// 之后:
createUser(user);
updateUser(user);
10.4 早返回 vs 深嵌套
// 之前:
if req != nil {
if req.Valid() {
process(req)
} else {
log.Warn("invalid")
}
}
// 之后:
if req == nil { return }
if !req.Valid() { log.Warn("invalid"); return }
process(req)
10.5 魔法数字 → 有名常量
// 之前:
if (retryCount > 3) backoff(5000)
// 之后:
const MAX_RETRY = 3
const BACKOFF_MS = 5000
if (retryCount > MAX_RETRY) backoff(BACKOFF_MS)
10.6 命名显式副作用
// 之前:看似获取,实际上可能会创建
Session s = getSession(userId);
// 之后:
Session s = createOrGetSession(userId);
10.7 返回 null → 空集合/Optional
// 之前:
List<Item> items = repo.find(...);
if (items == null) return Collections.emptyList();
// 之后:仓库层直接返回空集合,调用方无需判空
List<Item> items = repo.find(...); // 约定永不为 null
// 可选值用 Optional
Optional<User> user = userRepo.findById(id);
10.8 抽离 try/catch 到边界
// 之前:业务方法里到处 try/catch,主路径被打断
public Result transfer(...) {
try {
debit(); credit();
} catch (InsufficientBalance e) {
return Result.fail("INSUFFICIENT");
}
return Result.ok();
}
// 之后:业务层保持直观;异常在控制器/拦截器统一转换
public void transfer(...) {
debit();
credit();
}
@ExceptionHandler(InsufficientBalance.class)
public ResponseEntity<?> handle(InsufficientBalance e){
return ResponseEntity.badRequest().body(Map.of("code","INSUFFICIENT"));
}
十一、附录
11.1 命名词汇表模板(团队一起维护)
- 核心对象:Order / Invoice / Customer / Payment
- 常用动词:create / update / delete / calculate / authorize / settle
- 状态枚举:PENDING / APPROVED / REJECTED / CANCELLED
- 约定词:idempotencyKey / retryPolicy / cutoffTimeUtc
维护建议:新增术语先评审;全仓统一替换;避免同义多词并存。
11.2 Pull Request 模板(简版)
- 变更目的(业务价值/修复问题)
- 设计要点(边界、取舍、兼容性)
- 主要修改(模块/类/接口)
- 测试说明(覆盖点、异常路径)
- 风险与回滚方案
结语
把代码当“文章”来写:
- 一句话一行,一段话一个函数。
- 先把故事讲完整,再把每个段落写清楚。
- 每次提交,让代码“比来时更干净”。
这就是整洁代码最简单、也最有效的路径。
参考文献
- Robert C. Martin. Clean Code: A Handbook of Agile Software Craftsmanship.

1051

被折叠的 条评论
为什么被折叠?



