一、背景

作为程序员我们每天写代码、维护历史代码,有的公司甚至以代码量作为产出的一个指标。但是不同人代码质量却有云泥之别。

特别是维护遗留项目,一般历经多年,多人维护甚至多个团队维护过。修改Bug或者增加新功能时基本不敢动已有的逻辑,而是拷贝一份已有代码并在此基础上继续打补丁。甚至有一些最佳实践比如增加业务开关避免补丁引起线上故障。

这就是在已有的代码上做加法,也是大家笑称中的代码“屎山”。笔者上一家公司中多个产品项目都遇到类似问题。

二、原因

我们说需求是迭代的,架构和系统是演进的。产品在不同阶段有不同的约束,比如有的阶段是倒排期按时上线,有的阶段是提高质量、代码重构。总的来说,所谓“屎山”代码都不过是历史技术债务。出来混,迟早都是要还的。

根本原因是不了解已有的代码逻辑,不了解需求,对解决问题的思路没有深入的思考。

本文主要是从解决问题的角度来说。只有对需求和问题深入的理解后,对比不同的解决方案我们才能写出精简的代码,也就是对代码做减法

这是需要进行代码和算法的思维训练。LeetCode是一个不错的途径。

以今天的LeetCode“每日一题”为例,3100. 换水问题 II

1
2
3
4
5
6
7
8
9
10
给你两个整数 numBottles 和 numExchange 。

numBottles 代表你最初拥有的满水瓶数量。在一次操作中,你可以执行以下操作之一:

喝掉任意数量的满水瓶,使它们变成空水瓶。
用 numExchange 个空水瓶交换一个满水瓶。然后,将 numExchange 的值增加 1
注意,你不能使用相同的 numExchange 值交换多批空水瓶。例如,如果 numBottles == 3 并且 numExchange == 1 ,则不能用 3 个空水瓶交换成 3 个满水瓶。

返回你 最多 可以喝到多少瓶水。

例子:

1
2
3
输入:numBottles = 13, numExchange = 6
输出:15
解释:上表显示了满水瓶的数量、空水瓶的数量、numExchange 的值,以及累计喝掉的水瓶数量

最简单就是按例子模拟写。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution:
def maxBottlesDrunk(self, numBottles: int, numExchange: int) -> int:
res = 0
# 每次可交换的空瓶数量
exchange = numExchange
# 满瓶数量
full = numBottles
# 空瓶数量
empty = 0
while full > 0 or empty >= exchange:
# 有满瓶时
if full > 0:
empty += full
res += full
full = 0
# 可换瓶时
if empty >= exchange:
empty -= exchange
full += 1
exchange += 1
return res

上面的代码能通过LeetCode的Access,但是存在无效代码,逻辑比较绕,写完不确定是否考虑全边界Corner Case。

如果看推荐题解的话,就会发现差距。推荐题解如下:

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def maxBottlesDrunk(self, numBottles: int, numExchange: int) -> int:

ans = numBottles
empty = numBottles

while empty >= numExchange:
ans += 1
# numExchange空瓶子换回一瓶满的,再喝掉
empty -= numExchange - 1
numExchange += 1
return ans

官方题解很简洁。程序退出条件我们只需要关注空瓶子的数量。答案我们只需要关注“满瓶”数量,每次交换增加一个瓶子(第一次加上全部满瓶)。

三、总结

写代码无论是新需求还是维护历史代码,需要明确需求背景,深入理解问题,对比不同解决方案找最优,才能写出最简洁的代码。

这也许就是程序员的“第一性原理”吧。