当前位置:首页>java>如何写出高可读性的代码

如何写出高可读性的代码

  • 2026-09-29 13:30:02
如何写出高可读性的代码

如何写出高可读性的代码

  • 为什么可读性重要?
    • 因为一段代码的读次数比写入次数多很多,代码由多个人维护,很长一段时间内需要变更,那么可维护性非常重要
    • 如果可读性差,会导致看不懂,进而改不动。或者改起来风险高。得先读懂,才能去修改
  • 可维护性比性能优化要优先考虑,仅当性能不满足业务需求时,通过性能测试定位瓶颈,再针对性优化,避免“过早优化”

本文结合工程实践经验,从命名、注释、变量、架构、格式这几个维度,梳理一套通用且落地性强的代码规范

本文以go语言为主,但代码可读性的思想在各个语言是相通的,不影响理解

一、命名规范

1. 遵循规范

  • 遵从编程语言的原生规范:例如用i/j代表循环变量、k代表Map的键、v代表Map的值,符合开发者的通用认知
  • 团队内统一命名口径:借助业务词汇表统一语言,比如所有事务相关变量统一命名为tx,上下文变量统一命名为ctx,让代码逻辑可预测

2. 命名要“有意义、无歧义”

  • 拒绝无意义命名:工程代码中避免使用a/b/c这类模糊命名,算法题场景除外。例如:
// 差的写法:v的含义模糊,代码块变长后易混淆for _, v := range userList {// 业务逻辑}// 好的写法:直接体现变量含义,可读性提升for _, user := range userList {// 业务逻辑}
  • 避免二义性:例如同一方法内,block不能既表示“文件块号”,又表示“磁盘块号”,需通过前缀区分(如fileBlock/diskBlock)
  • 通过方法名展示方法意图,例如:getXXXFromCache(明确数据源)、fetchXXX(强调远程获取)、computeCostTime(强调计算逻辑)。而不是没有明确含义的handeXXX

3. 简洁性

  • 利用上下文缩短命名:
    • 在RuneCount方法中,结果变量命名为cnt即可,无需冗长的runeCount
    • 包名已体现业务时,类型/方法名需精简(如包名dingding,接口名无需重复写dingdingRobotService,直接用RobotService,避免调用时出现dingding.dingdingRobotService.XXX的冗余写法)
  • 采用通用缩写:参数用req(request)、返回值用resp(response),符合行业惯例
  • 命名长度与使用距离匹配:变量使用位置离声明越远,名称应越长;局部短生命周期变量可适当简化(如方法内临时计数变量用i,全局统计变量用totalUserCount)

4. 命名与实际含义一致

  • 变量名需精准匹配内容:
    • 不能出现“变量名是userIds,实际存储userInfos”的情况,这种错误会直接误导代码阅读者
    • 方法名为getXXXFromDB,但实际从缓存查询
  • 这类问题常发生在需求迭代时:修改了变量存储的内容,却忽略了同步修改变量名

二、注释是“更高层的抽象”

优秀的代码本身应具备可读性,注释仅用于补充代码无法表达的信息,而非重复描述代码逻辑

1. 注释的适用场景

  • 补充抽象信息:方法声明(名称、参数、返回值)无法完整体现的逻辑,需用注释说明——比如API用法、方法间的依赖关系、核心流程的分段总结
  • 解释“为什么”:代码只能体现“做什么”和“怎么做”,特殊逻辑的设计原因(如“此处加锁是为了避免并发修改缓存”)必须通过注释说明,让阅读者与编写者掌握同等信息

2. 不建议写注释的场景

  • 无需注释“显而易见”的逻辑:比如给i++加注释“i自增1”,纯属冗余。
  • 优先用代码替代注释:
    • 魔法数字用常量替代:const secondsInDay = 86400 替代 // 86400代表一天的秒数;
    • 复杂逻辑用函数封装:将一段判断逻辑提取为函数,用函数名替代注释,例如:
// 重构前:注释才能说明逻辑含义publicvoidapplyFee(Account account){    if (account.getBalance() < 0 && account.isOverdraftEnabled()) {                account.addFee(OVERDRAFT_FEE);        }}// 重构后:函数名即注释,代码更简洁publicvoidapplyFee(Account account){    if (shouldApplyOverdraftFee(account)) {                account.addFee(OVERDRAFT_FEE);        }}privatebooleanshouldApplyOverdraftFee(Account account){    return account.getBalance() < 0 && account.isOverdraftEnabled();}

3. 注释的注意事项

注释必须与代码同步更新:迭代代码时若修改了逻辑,需同步修改注释,避免注释成为误导

三、变量规范

1. 就近声明,最小化作用域

变量在哪使用就在哪声明,避免提前声明大作用域变量。例如:

// 差的写法:提前声明,作用域覆盖整个方法funchandleOrder() {var totalAmount float64// 大量无关逻辑...    totalAmount = calculateAmount(order)// 使用totalAmount}// 好的写法:就近声明,作用域仅包含使用逻辑funchandleOrder() {// 大量无关逻辑...    totalAmount := calculateAmount(order)// 使用totalAmount}

2.尽量不使用全局变量

全局变量会与所有函数耦合,成为“隐形参数”——一旦全局变量修改,所有依赖它的函数都可能出错

优先通过参数传递、结构体封装等方式替代全局变量

3. 提炼高可读性的变量

将复杂表达式的结果赋值给临时变量,提升代码清晰度:

// 重构前:表达式冗长,可读性差if (order.getTotalPrice() - order.getDiscounts() > 100) {    // 逻辑处理}// 重构后:变量名体现含义,逻辑一目了然double netPrice = order.getTotalPrice() - order.getDiscounts();if (netPrice > 100) {    // 逻辑处理}

四、代码架构规范

1. 最小化原则,降低包的表面积

  • 常量拆分:通用常量可放在公共包,业务常量按模块/层级拆分(如service层和dao层分别维护常量),避免单个常量文件臃肿、命名冲突。
  • 内部结构体私有化:仅在方法内使用的结构体,声明为私有(如Go中用小写开头),避免其他包误用。

2. 避免跨层依赖

遵循“依赖倒置”,上层仅依赖下层抽象,不直接依赖下层的第三方依赖。例如:

  • DAO层使用GORM框架,若查询无结果返回gorm.RecordNotFound,DAO层需封装为自身的RecordNotFound错误返回给Service层;
  • 若Service层直接依赖gorm.RecordNotFound,当DAO层替换ORM框架(如从GORM改为XORM)时,Service层需全量修改;而依赖DAO层封装的错误,仅需修改DAO层即可。

3. 杜绝循环依赖

循环依赖会导致模块“相互影响”:上层依赖下层时,上层崩溃不影响下层;但循环依赖下,任一模块出错都可能引发连锁反应。例如:

  • 错误示例:user模块依赖order模块,order模块又依赖user模块;
  • 解决方式:抽离公共逻辑为独立模块(如common),让user和order都依赖common,消除循环。

4. 避免代码重复

重复代码的核心问题是“修改时容易漏、逻辑不一致”。例如:两处实现“订单金额计算”的代码,若仅修改一处,会导致不同场景下金额计算规则不一致

为什么会产生重复代码?当发现需要的功能正好和某处很相似时,看起来最简单的做法就是:将其复制过来,改几个地方,测试下,就收工

解决方式:将重复逻辑封装为公共方法,所有场景统一调用,实现逻辑“收口”

五、代码格式与结构:降低阅读的“认知负荷”

1. 统一格式规范

  • 借助工具格式化:Go语言用go fmt统一代码样式,避免因风格差异增加阅读成本;
  • 行长度限制:每行代码不超过120个字符,避免横向滚动;
  • 空行分隔逻辑块:不同业务逻辑、函数段落间留空行,让代码结构更清晰。

2. 杜绝魔法数字

魔法数字(无解释的硬编码数字)是维护噩梦,需用常量封装:

// 差的写法:4的含义不明确if order.Status == 4 {// 处理已完成订单}// 好的写法:常量名自解释,修改时仅需改常量值const OrderStatusCompleted = 4if order.Status == OrderStatusCompleted {// 处理已完成订单}

3. 使用短函数

长函数的缺点:

  • 函数太长会导致可读性下降,难以维护
    • 如果函数20行左右,扫一眼就能理清逻辑
  • 测试困难:无法只测其中的一部分
  • 把多个业务处理流程放在一个函数里实现。把不同层面的细节放到一个函数里实现,分离关注点没做好
    • 一个人能理解的东西是有限的,没有人能同时面对所有细节
// 差的写法:函数包含多层面细节,逻辑混杂funchandleTrade() {// 1. 日期转换(基础细节)    startTime, _ := time.Parse("2006-01-02", req.StartDate)// 2. 校验订单(业务逻辑)if req.Amount <= 0 {return errors.New("订单金额不合法")    }// 3. 扣减库存(核心逻辑)    stockService.Deduct(req.GoodsID, req.Num)// 4. 生成交易记录(持久化逻辑)    db.Create(&TradeRecord{GoodsID: req.GoodsID, Amount: req.Amount})}// 好的写法:拆分函数,每个函数仅做一件事funchandleTrade() {    startTime := parseStartDate(req.StartDate)if !validateOrderAmount(req.Amount) {return errors.New("订单金额不合法")    }    deductStock(req.GoodsID, req.Num)    createTradeRecord(req.GoodsID, req.Amount)}funcparseStartDate(dateStr string)time.Time { /* 日期转换逻辑 */ }funcvalidateOrderAmount(amount float64)bool { /* 金额校验逻辑 */ }funcdeductStock(goodsID int, num int) { /* 扣减库存逻辑 */ }funccreateTradeRecord(goodsID int, amount float64) { /* 生成交易记录逻辑 */ }

这样修改后:

  • 人们面对的就不再是细节,而是模块,模块的数量显然会比细节数量少,人们的理解成本就降低了
  • 当阅读代码的人对某一行感兴趣时,才跳转到底层去看怎么实现的,不会干扰到阅读当前层的逻辑。而不是把所有细节都在当前层事无巨细地展示出来,强迫阅读者阅读底层代码
  • 小函数也方便写单测,容易提高代码质量
  • 拆解长函数后,变量命名可以更短,理解的成本也相应地会降低。因为变量都是在这个短小的上下文里,也就不会产生那么多的命名冲突,变量名当然就可以写短一些

从流程上解决:用圈复杂度限制来准入代码

4. 减少缩进

过多缩进会让代码“层层嵌套”,可读性骤降,可通过“卫语句”优化,尽量提前返回

优化完成后:

  • 少了很多else逻辑,可读性较高
  • 正常逻辑在代码布局上始终靠左,读者一眼就能看清主干逻辑
// 差的写法:多层嵌套,主干逻辑不清晰funcGetEnvName()string {if IsOnline() {return"online"   } else {if IsLocalHost() {return"localhost"      } else {return"test_" + GetShipName()      }   }}// 好的写法:卫语句提早返回,逻辑层级扁平化funcGetEnvName()string {if IsOnline() {return"online"   }if IsLocalHost() {return"localhost"   }return"test_" + GetShipName()}

5. 表驱动编程:分离逻辑与数据

将“判断逻辑+静态数据”分离,用映射表替代大量if-else/switch,提升扩展性:

// 差的写法:逻辑与数据混合,新增月份需修改代码funcgetMonthName(month int)string {if month == 1 {return"Jan"    } elseif month == 2 {return"Feb"    }// ... 更多月份判断return"Invalid month"}// 好的写法:表驱动,新增月份仅需修改映射表var monthNameMap = map[int]string{1:  "Jan",2:  "Feb",3:  "Mar",4:  "Apr",5:  "May",6:  "Jun",7:  "Jul",8:  "Aug",9:  "Sep",10: "Oct",11: "Nov",12: "Dec",}// 使用的地方不需要修改funcgetMonthName(month int)string {if name, ok := monthNameMap[month]; ok {return name    }return"Invalid month"}

六、方法设计规范

1. 参数设计:清晰、灵活、安全

  • 参数名体现角色:例如copy(dest, src)而非copy(a, b),明确“目标”和“源”;
  • 依赖抽象而非具体:提升方法扩展性,例如:
// 差的写法:依赖具体的os.File,仅能写入本地文件,测试困难funcSave(f *os.File, doc *Document)error {// 写入逻辑}// 好的写法:依赖io.Writer抽象,可写入文件、网络、内存等,测试更简单funcSave(w io.Writer, doc *Document)error {// 写入逻辑}
  • 避免any类型:
    • any没有类型检查,容易出错
    • 想看该方法依赖的参数时,根据无从下手
  • 慎用指针参数:避免方法内部修改参数内容,如需修改,优先通过返回值传递
  • 精简参数个数:参数过多(超过6个)会增加调用和维护成本。造成很多参数的原因是:每次只增加一点点,累积起来,便不忍直视了。解决方式:
    • 封装为请求结构体:将多个参数封装为XXXReq结构体,新增参数时无需修改函数签名
      • 特别是函数调用层级很深的场景下,能降低不少改动量
    • 使用Option模式:适用于参数可选、组合灵活的场景

2. 封装性:暴露最少必要信息

封装性好的优势:

  1. 减少调用方的认知负荷:即使不知道模块内部的细节,也能使用

  2. 可以轻松替换内部的实现,不必担心对使用者造成影响

  • 示例1:HTTP框架提供getParameter(String name)获取参数,而非直接返回底层的参数Map

    • 用户根本不关心底层怎么存储的,只关心某个参数的值是啥
  • 示例2:Linux文件系统的read/write接口,没有暴露磁盘读写的底层逻辑

  • 示例3:Go的GC机制,仅暴露少量必要调优参数,屏蔽底层实现细节

最新文章

随机文章