代码整洁之道-提要

这份文档不绑定任何具体项目或语言。目标是:让你写出“别人一眼就懂”的代码。
这份文档只专注于代码可读性、可维护性
本文内容为方法论参考,实践中请根据项目实际情况权衡使用,避免完全的机械套用。

你将收获什么

  • 一套简单好记的写码习惯与检查清单
  • 大量“前后对比”的示例(看得懂、照着用)
  • 一条从“草稿”到“整洁”的重构路径
  • 想先看效果请直接翻到第七节

一、总目标(为什么要整洁)

整洁代码的标准很朴素:

  • 别人看到后,10 秒内能回答两个问题:
    1. 这段代码在做什么? 2) 为什么要这样写?
  • 读者能快速定位“哪里需要改”“怎么改不会出错”。

整洁 ≠ 花哨的排版。整洁 = 清晰的表达 + 合理的结构 + 可预期的行为。

给自己三条底线:

  • 不让读者猜(用清晰的名字和结构表达意图)
  • 不给读者惊喜(函数名和行为一致,没有隐藏副作用)
  • 不留坑给读者(不返回/传递 null,不埋魔法数,错误有清晰提示)

二、命名(让名字说人话)

核心思路:名字要“名副其实”,让人一眼看出意图。

2.1 基本规则

  • 用能读出来的名字:避免 genymdhms,改成 generateTimestamp
  • 不要把类型写进名字:nameString,String 多余,类型由语言/IDE 知道。
  • 避免空话:Data/Info/Manager/Util 意义不清。更具体一些,比如 SnapshotCalculatorAuthorizer
  • 类/对象名用名词或名词短语:OrderAddressBookPricePolicy
  • 方法名用动词或动词短语:calculateTotalloadConfigsaveUser
  • 布尔返回的方法建议以 is/has/should/can 开头:isEligibleshouldRetry
  • 别害怕长名字:清晰优先于短小,能一眼看懂最重要(例如 calculateMonthlyRepayment)。
  • 按业务起名:尽量使用领域内大家都懂的词(统一语言),减少歧义。

2.2 可搜索性

  • 避免单字母变量和“魔法数字”。把数字提成有名常量:MAX_RETRY = 3
  • 项目里出现过的同一概念,尽量使用同一个词,不要时而 customer 时而 client

2.3 加语境(上下文)

  • 零散字段可加前缀:addrStreetaddrCity
  • 更好的做法:引入值对象 Address,字段内部就用 street/city/zip

2.4 名字长度与作用域

  • 作用域越大,名字越要清晰(可能越长)。循环里的 i/j 可以,跨方法的变量就不行。

2.5 不要掩盖副作用

  • 如果方法会创建对象,就别叫 getX();请叫 createOrGetX(),把副作用说清楚。

2.6 集合与单位

  • 集合用复数:usersorders
  • 明确单位:timeoutMsintervalSecondsamountCents
  • 避免容器后缀:优先用语义表达集合(accounts 而非 accountList)。
  • 不可数名词的集合(如 equipment、staff):使用“可数的分类词”来命名,如 equipmentItemsstaffMembers,或使用更具体的集合名词 inventorypersonnel
  • 去掉冗余词: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.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值