工程师的“好代码”之路:从新手认知到多维考量

工程师七年思考“好代码”,从完成任务到多维评价,最终是权衡的艺术与团队共识。

原文标题:一位工程师对“好代码”的 7 年思考

原文作者:阿里云开发者

冷月清谈:

这篇文章记录了一位工程师七年来对“什么是好代码”的深度思考。作者回顾了从初入职场时仅关注完成任务的“黑盒认知”,逐步发展到从多个维度审视代码质量的转变。他详细探讨了代码从运行、排查到复用过程中的可观察指标,并分享了阿里“金码奖”对好代码的四大共识性评价标准:稳定性、体验、效率和成本。文章还从全局理解、避免“坏味道”及应用设计原则与模式等角度,阐述了如何写出好代码。最后,作者提出写好代码是一门艺术,涉及层次设计、易理解性与非功能性需求间的复杂权衡,强调没有一劳永逸的标准答案,而是根据目标和成本不断调整的动态过程。

怜星夜思:

1、文章提到了开闭原则:“软件实体对扩展开放,对修改封闭”。但在实际工作中,我们经常会遇到为了快速响应业务需求,不得不修改旧代码的情况。大家有没有觉得,在你们的项目中,哪些场景特别难以完全遵循开闭原则?有没有为了“赶DDL”而“打破”原则的经历?这种情况下,你们通常会怎么做来平衡原则和现实?
2、文章列举了魔法值、`Collection.contains`低效等“坏味道”。在我们日常的开发和Code Review中,除了文中提到的,你觉得最常见的、或者说最让你头疼的“坏味道”是什么?为了避免这些“坏味道”,除了人工Review,大家有没有用过什么“神器”级别的代码质量分析工具或者自动化检查工具?效果怎么样?
3、文章提到交易链路中可能会有系统应用、服务入口、流程服务、活动节点、领域服务等多个层次,导致上下文透传困难,甚至要用ThreadLocal。大家在自己的项目中,有没有遇到因为“层次过多”导致反而增加了理解和维护成本的情况?你们觉得,怎么样的分层才是“恰到好处”的?在追求架构优雅和实际开发效率之间,你们倾向于如何平衡?

原文内容


阿里妹导读


本文围绕“什么是好代码”展开,作者结合自身职业发展阶段,从初入职场时仅关注完成任务的“黑盒认知”,逐步过渡到深入思考代码质量的多维度评价标准。


一、前言

么是好代码?这个问题很基础,但工作那么多年,的确没有认真细想过。很高兴能有机会进行一下整理,并和大家分享一下我对好代码的理解,下面是本文的提纲,文中也引用了一些别人的材料,希望能够给大家一点点启发或者产生共鸣。


二、什么是好代码

2.1 回忆入职阶段

2.1.1 认知黑盒:完成任务就算好代码

刚入职的时候,我们对新的工作环境,协作关系,系统结构等都比较陌生。周围的环境对我们来说是一个黑盒,我们不知道系统是如何交互的,交互的协议是怎么一步步建立起来的,有哪些特殊的场景,所以只能在局部小心意义地添加代码,生怕出事。

回忆那个时候,我当时首先考虑的是能够完成任务,思考的几个问题如下:

1. 代码应该写在哪里? - 期望:理解链路 实际:问师兄

2. 代码应该怎么写? - 期望:理解上下文 实际:拷贝周围

3. 代码不要出错? - 期望:做好边界判断 实际:review 被提示

4. 代码可以回滚? - 期望:做好开关 实际:过于自信

循环:开关应该怎么弄? - 重复 1 2 3

那时候,理解的好代码,是最基础的要求:支持需求,不要出错。

2.1.2 了解增多:开始思考什么是好代码
认知增多

后来逐步开发,了解增多后,开始反转了:不知道的变成少数(注:某些特定系统,不敢说全链路....), 知道的变成了多数。这时候,我们能够思考的内容就变多了,同时开发的体验也增多了,就会产生一些疑问:

1. 支持需要? 这次是支持了,但是造成下次改动更加困难了, 这样的代码是好代码么?

2. 不要出错? 业务功能可能没有出错,但是可能造成性能问题, 这样的代码是好代码么?

我们渐渐从单一的完成任务,开始思考如何更好地完成任务,从短期的设计开始考虑长期的感受,从单一的指标开始考虑更多的因素,那么好代码的答案会是什么呢?我们可以继续往下探讨。

2.2 评价代码的一些维度

理解代码是好代码还是坏代码,可能是比较困难的一件事情,因为考虑的因素有很多,每个人的看法也不太一样,那该如何评价呢?

下面的一个评价标准,可能是抽象到最高程度了:

Martin(Bob大叔)曾在《代码整洁之道》一书中说:当你的代码在做 Code Review 时,审查者要是愤怒地吼道:“What the fuck, is this shit?”、“Dude, What the fuck!”等言辞激烈的词语,那说明你写的代码是 Bad Code,如果审查者只是漫不经心的吐出几个:“What the fuck?”,那说明你写的是 Good Code。

图片来我心中的好代码》

虽然比较抽象,但是我们还可以从载体去看,如果需要评价,那么就需要衡量的指标,要产生对应的指标,就需要被观察到,从可被观察到的角度看,我们可以看看代码的一些动作:

代码被观察

  • 当我们上线服务的时候,代码会被 「运行」,产生系统运行指标,同时也会影响用户体验。

  • 系统指标:比如耗时很长,可能没有单元化调用,跨单元了;

  • 用户体验:比如可能提示的错误码就是 System Error,  用户难以理解。

  • 当我们遇到问题的时候,代码会被「排查」,让我们感受到排查难以程度。

  • 排查难易:比如很难找到关键日志,找到日志后,代码也没有很好的分块,有很多 if  else。

  • 当我们遇到类似需求时,代码会被「复用」,会影响复用的成本。

  • 复用成本:如果责任不单一,抽象粒度不够,就可能会引起较多老代码修改,增加风险和变更压力。

2.3 代码评审标准介绍

上面列举了评价的一些维度,进一步看,我们想要的这个评价不仅仅是单个人的观点,更是大家的共识。就比如一道菜,你说不咸,但是其他人都说咸,整体来说就是偏咸的。所以我们得意识到个人的判断是带有主观性的,大家的共识可能更客观一点。

之前参加了部门“金码奖”代码评审,所以这里也简单分享一下评审规则,也感受一下大家的共识:

好代码的评选标准回到业务价值 — 体现在稳定体验效率成本四个维度

角度

解释

关键点举例

稳定40分

代码在运行过程中具备高可用性和容错能力...... 从而保障业务连续性和系统连续可用。

错误处理&边界保障:

  • 应包含覆盖边界条件(如空值、越界、非法输入)。

  • 异常处理应完备(如 try/catch 处理、错误码返回)。

监控日志完备:

  • 具备完善的日志记录和系统监控,便于问题排查和定位。

冗余设计&版本控制:

  • 设计备用路径(如双库双活、异地容灾)。

  • 本控制和回滚机制。

体验30分

代码实现是否能够提升用户的满意度,包括功能易用性性能表现、交互流畅性、新用户体验创造等。

性能优化适配:

  • 对部分性能不足设备进行减少冗余计算/请求(如懒加载、数据压缩)设计。

  • 针对高频操作进行缓存设计 ......

用户友好反馈:

  • 用户操作更人性、更快捷,更简单(功能表达足够清晰)。

  • 出现问题时提供友好且有帮助的错误提示(在用户可理解的语言在和用户对话)

......

效率20分

指代码能否高效支持业务迭代和运行,包括:

  • 开发效率(代码编写、维护的成本)。

  • 运行效率(性能、资源利用率)。

  • 运维效率(监控、故障排查的便捷性)。

性能优化

  • 代码避免重复逻辑(如代码复用、抽象公共模块)......

  • 选择了最优算法(如时间/空间复杂度).....

维护效率:

  • 代码结构合理,支持自动化测试、部署、监控告警,减少维护时间提升维护效率。

可扩展性 &业务提升便捷

  • 计灵活,支持扩展,避免瓶颈,降低开发成本,提升处理速度,增强系统性能,支持快速迭代。

成本10分

指在资源、运维、人力成本等维度的降低或优化,包括直接投入和隐性运维成本

资源优化:

  • 如避免冗余数据库查询、减少无用计算。

  • 合理使用缓存降低后端压力,降低服务器、带宽等基础设施支出。

维护成本&知识传递:

  • 具备完善的文档、代码注释知识管理,降低团队成员协同成本。

测试成本:

  • 通过自动化测试或其他手段,减少人工测试成本。


三、如何写好代码

有了一个基本的认知后,下面将结合一些大家和个人的总结,再一起看看写好“好代码”的心得。

3.1 全局理解“好代码”

“神之一手”是源自日本动漫《棋魂》的围棋术语,指棋手在关键时刻以超乎常理的技艺走出扭转全局的一步,象征围棋技艺的最高境界。

于每一步棋来说,我们只能看到局部的影响(受限于我们能想到后面几步),当我们事后站在全局的角度看,有可能的“平平无奇”的一步却起着关键性的作用。对代码里来说,我理解的“好代码”,应该在更大的范围来看,确保每一步在大局中的“精确性”,不“草率落子”,也不“过度设计”,让大家感觉“恰到好处”,很舒服。

为了理解当前的代码,我们要站在更高的层次,从开发的角度看,关注点可以有这么几个层次:

《程序员的自我修养 - 架构主题简思》

  • 设计模式关注类之间的关系,探究类之间如何协作完成信息传递与计算。

  • 应用内架构关注应用内的结构,可以体现在模块间的设计上。

  • 应用间模式关注应用间协作,将功能划分到不同应用中,设计应用间协作完成复杂流程。

  • 业务架构关注系统间协作,将商业活动分解到不同系统中,通过系统协作支撑价值流。

关于如何具体如何养成标准,我找到了一篇比较好的文章 《我心中的好代码》,里面引用了很多内容,可以大家在做好每一步时对齐的标准:

《我心中的好代码》

个人经历映射

范围

关注点

实践参考举例

我的文章举例


变量、方法、类

阿里经济体开发规约

开源版本:阿里巴巴java开发手册 https://github.com/alibaba/p3c

《[实验] 金额对象类型问题》

线

方法、类之间关系及数据结构

《Java设计模式:23种设计模式全面解析(超级详细)》https://c.biancheng.net/design_pattern/

《设计模式六大原则理解》

服务/应用(模块之间耦合关系)

《应用架构之道:分离业务逻辑和技术细节》

《应用架构实践 - 简洁应用框架 VSEF》

从业务中高度抽象出本质模型

《复杂性应对之道 - 领域建模》

《DDD的关键理解》


3.2 “坏味道”优化

前面直在介绍“好代码”,其实避免“坏代码”,我们也就能把代码往好的方向推进了。下面会介绍一下代码的“坏味道”,大家可以一起看下体会下。下面的例子来自《》。

3.2.1 影响可读性

例1:L大小写

在使用长整型常量值时,后面需要添加L,必须是大写的L,不能是小写的l,小写l容易跟数字1混淆而造成误解。

反例

long value = 1l;

正例

long value = 1L;

案例2:不要使用魔法值

当你编写一段代码时,使用魔法值可能看起来很明确,但在调试时它们却不显得那么明确了。这就是为什么需要把魔法值定义为可读取常量的原因。

反例:

for (int i = 0; i < 100; i++){
    ...
}
if (a == 100) {
    ...
}
正例:
private static final int MAX_COUNT = 100;
for (int i = 0; i < MAX_COUNT; i++){
    ...
}
if (count == MAX_COUNT) {
    ...
}
3.2.2 性能

案例:频繁调用Collection.contains方法请使用Set。

在java集合类库中,List的contains方法普遍时间复杂度是O(n),如果在代码中需要频繁调用contains方法查找数据,可以先将list转换成HashSet实现,将O(n)的时间复杂度降为O(1)。

反例:

ArrayList<Integer> list = otherService.getList();
for (int i = 0; i <= Integer.MAX_VALUE; i++) {
    // 时间复杂度O(n)
    list.contains(i);
}

正例:

ArrayList<Integer> list = otherService.getList();
Set<Integer> set = new HashSet(list);
for (int i = 0; i <= Integer.MAX_VALUE; i++) {
    // 时间复杂度O(1)
    set.contains(i);
}
3.2.3 原理Bug

例:禁止使用构造方法BigDecimal(double)。

BigDecimal(double)存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。

反例:

BigDecimal value = new BigDecimal(0.1D);//0.100000000000000005551115...

正例:

BigDecimal value = BigDecimal.valueOf(0.1D);//0.1

3.3 交易链路中的设计原则和模式

学习的内容,如果能和我们的工作内容相关联,那么我们能够吸收的更快一点。写好代码的一个比较重要的部分在于掌握常用的设计原则和模式。

关于设计原则和设计模式,我之前也在交易链路上尝试寻找,进行进一步的理解,具体文章可以参考 ,下面会举一两个例子来体会一下多个类/模块之间的合作设计。

3.3.1 设计原则举例:开闭原则

闭原则,在面向对象编程领域中,规定“软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的”,这意味着一个实体是允许在不改变它的源代码的前提下变更它的行为。

实践举例:订单列表中,平台定义需要展示店铺图标的框架,业务可以自助定制店铺图标。

3.3.2 设计模式举例:责任链(Chain of Responsibility)

责任链是说将请求让队列内的处理器一个个执行,直到找到愿意执行的。

商业能力扩展、域扩展,在执行回收结果的时候,会遍历实现的插件,并结合回收规则,进行及时的熔断。这和责任链的逻辑是类似的。实践举例:以确认收货打款时“是否跳过通知支付”为例(如:有些是即时到账场景),执行引擎会遍历插件的实现,找到第一个返回要 true(跳过)的结果时,就会停止执行,整体返回 true。


四、写好代码是一门艺术

很多时候,代码怎么写是受到很多因素影响的,取决于我们想要的目标,当然也需要承担对应的成本。就像下面的神经网络一样,最终输出的结果是多种因素变量 和 权重 结合的复杂结果,是一个取舍。

4.1 层次是否过多

计算机科学中的所有问题都可以通过增加一个间接层来解决。

好的分层,可以让大家遵循统一的分解过程,在任务维度进行更好地复用。在结构上也更清晰。

是“成也萧何,败也萧何”,交易链路上,比较痛苦的地方在于:层次过多! 有系统应用、服务入口、流程服务、活动节点、领域服务、领域能力、领域扩展、商业能力扩展、商业能力实现这一系列层次。

这样的层次,导致透传一个上下文,需要层层透传,进行打包,搞个一天起步,所以也经常看见基于 ThreadLocal 这样的缓存方案。这种透传是否合理呢?层次多到一定程度之后,是不是理解负担进一步加重?

4.2 易理解性的挑战

代码的易理解性是最素的要求,但是做好这一点也非常有挑战性。

首先,我们看一下一个轻量脚本框架的理解,看看其对可读性的影响。轻量脚本的优缺点如下:

  • 好处:能够在一个配置化后台,用比较简单的配置动态生成脚本,减少打包代码部署流程较长的烦恼。

  • 缺点:关键的业务逻辑被包装在了一个脚本化框架体系内,需要额外的学习成本。业务开发人员解释不了整体的逻辑,需要找脚本框架维护人员进行答疑。

易理解性的挑战1 - 脚本化

个人观察:

  • 在层次较多的系统中,谨慎增加额外层次(灵活性的收益 是否能够 覆盖理解成本);

  • 引入框架,需要判断框架的可维护性,确认组织意志;

  • 考虑长期的维护效率,而不是某次的发布。

除了层次,非功能性需求对易于理解性的挑战也很大。下面在模拟了一些演化情况,来看看易理解性的挑战:

易理解性的挑战2 - 非功能性要求

个人观察:

  • 好的代码应该隔离关注点,把真正的业务逻辑呈现出来;

  • 性能、可用性等实现应该作为一些切面、策略独立出来,可选可去;

  • 代码在边界场景上的正确性,以及可读性处理,是一个比较进阶的阶段


五、小结

好的代码标准,随着考虑因素的变化,也在不停地调整。我们的工作经历,其实就是在不停地调整各个因素的权重,最终会给出我们的一个判断。

通过团队的多种判断碰撞,形成一个团队的共识,如果符合团队的共识规则,我想应该是一个比较基础的答案。更好的代码出现,改变组织效率,带来业务创新的情况,取决于各个人的发展情况和历史机遇。总而言之,你所在的位置,定义了你会写出怎么样的好代码。

颠覆传统 BI,用自然语言与数据对话


针对企业在数据分析过程中面临的取数难、报表效率低和数据割裂等问题,Quick BI 作为企业级分析 Agent,支持通过自然语言完成看板搭建与数据获取,借助 AI 发现异常并归因,真正实现“ChatBI,对话即分析”,显著提升数据使用效率与用户体验,助力企业高效运营、科学决策。


点击阅读原文查看详情。


开闭原则真是理想很丰满,现实骨感啊!我们团队在一些遗留系统上,真的很难做到完全对修改封闭。每次新需求一来,旧代码的逻辑简直就是牵一发而动全身。这种时候,如果时间真的很紧,我们会优先保证功能上线,但会立即把这次修改标记为短期技术债,等后续迭代或有专门的技术优化时间再考虑重构,尽量将改动的影响范围局部化,并添加完善的测试用例来保障稳定性。毕竟,活儿先干完,再想着漂亮。

哈哈,说到开闭原则,那不就是程序员中的“奥林匹克精神”吗?更高、更快、更强…咳,我是说,更高内聚、更低耦合。但现实往往是:你以为你写的是插件,结果客户的需求直接改了你的‘插座’!:joy: 这种时候,我就安慰自己,这不叫打破原则,这叫‘策略性妥协’。等DDL过了,就赶紧把那些‘妥协’的地方用注释圈起来,备注:‘此乃历史遗留的战损品,速来改造!’,希望能给后来的英雄提个醒。

我倒觉得最让人难受的是‘悄悄地’引入的性能陷阱,那种看似无害,但在大流量下能把系统卡死的代码。比如在循环里查数据库,或者N+1查询问题,平时小数据量根本看不出来。Code Review很难百分百发现。所以除了SonarQube这种静态分析,我们更注重单元测试和集成测试,以及JMeter之类的性能测试工具。把性能测试也前置到开发阶段,及时发现。毕竟,代码不光要写对,还要写得快!

我觉得‘注释缺失或过时’也是一种非常致命的‘坏味道’。没有注释的代码,就像没有地图的迷宫,每次接手新模块都得靠人肉‘考古’。即使有注释,如果跟不上代码的改动,那误导性比没有还糟糕。所以除了工具,我觉得更重要的是团队内部的Code Review文化和规范。比如我们强制要求复杂逻辑必须有详细的JIRA描述,并且代码注释要解释‘为什么这么做’而不是‘做了什么’。工具只能发现语法和结构问题,‘意图’的表达还得靠人。

哈哈哈,ThreadLocal透传上下文?那可太真实了!我们这儿为了应对‘层层嵌套’,有的老系统甚至直接在Session里塞业务数据,说是为了‘方便’,结果就是整个项目谁都不知道Session里到底有什么,哪里被改了。简直是‘一层套一层,层层裹危机’。我个人觉得,分层这东西,就像穿衣服,是为了保暖(管理复杂度)和好看(架构美观)。你不能为了好看冬天穿短袖,也不能为了保暖穿成个球。适度就好,够用就行。如果分了层反而更难理解,那肯定就本末倒置了。我们现在强调的是‘领域聚合’和‘边界上下文’,尽量让数据和行为在最小的合理范围内闭环,减少跨层级、跨模块的‘手递手’操作。

我深有体会!之前在一个电商项目里,为了所谓的‘领域驱动’,硬生生分了七八层,结果一个简单业务请求流转下来,好几个接口,光是DTO和VO之间来回转换就能把你搞晕。最要命的是,想调试某一块逻辑,得层层打断点才能搞清楚。后来我们痛定思痛,简化了分层,只保留了接口层、业务逻辑层和数据持久层这三到四层核心。我们的经验是,不要为了分层而分层,根据团队的能力和项目复杂度来定,能用更少的层级解决问题就用更少的。过度设计,才是最大的浪费。

确实,开闭原则的挑战主要集中在业务需求频繁变动且难以抽象出稳定接口的场景。像一些高度定制化、规则复杂的业务逻辑,每次变动都可能牵涉到核心流程。我们尝试引入DDS(Dynamic Decision System)或规则引擎,将业务规则外部化、配置化,这样在规则发生变化时,只需修改配置,而无需改动核心代码。另外,对于难以避免的代码修改,我们会尽量在接口层面保持稳定,将变动封装在实现类内部,并配合契约测试(Contract Testing)来确保兼容性。

最让我头疼的‘坏味道’,那必须是‘大泥球’类和‘上帝对象’,就是一个类包罗万象,几千行代码,方法巨多还互相调用,改起来心惊胆战。还有就是重复代码,Ctrl+C/V一时爽,后期调试火葬场。为了避免这些,我们团队主力用的是SonarQube,它能集成到CI/CD流程里,每次提交代码都会自动分析,找出复杂度高、重复代码、潜在bug等问题。虽然有时候它报的‘警告’有点多,但大部分情况下还是很有帮助的,至少能提醒我们去关注那些容易被忽视的角落。