ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从“能跑就行”到可维护易扩展:代码长期价值的关键

从“能跑就行”到可维护易扩展:代码长期价值的关键 代码能跑就行这句话我在新手群里见到的频率可能比救命还高。刚学会循环和函数能把一段代码跑出结果那种成就感确实很真实我完全理解。但作为一个被自己写过的烂代码坑过太多回的人今天想认真聊聊能跑的代码离真正能用的代码中间到底差了多远。可维护性和易扩展性这两个词听起来像软件工程里的概念讲的是规矩和长远价值但对初学者来说它们恰恰是决定你三个月后、半年后、甚至三年后还能不能看懂自己代码的分水岭。这不是什么高大上的理论我会用实际例子拆给你看。1. 能跑就行这个口头禅坑过多少新手1.1 我亲眼见过的一次代码翻车现场有一次帮一个学妹看她的期末大作业功能是学生成绩统计。她打开代码屏幕上洋洋洒洒几百行没有一个函数全是按顺序往下写的逻辑先是打开文件然后循环读数据然后求和、求平均最后print输出。我问她如果现在要加一个功能统计及格人数你打算改哪里她愣了几秒然后开始在代码中间翻找最后指着一行循环说可能在这附近加个判断吧。我又问那如果要把这个统计功能用在另一门课的成绩文件上怎么办她想了想说应该可以复制一份吧。这个场景我太熟了。代码确实能跑运行结果也对但任何一个新需求进来都要靠人肉定位 手动复制粘贴来完成。最关键的还不是麻烦而是改着改着就开始害怕——因为你根本不知道改这一行会不会影响别的结果。这就是典型的能跑就行埋下的雷当时跑得顺之后改得痛。1.2 能跑是下限不是上限我经常打一个比方能跑的代码等于一间能住人的毛坯房。水电通了马桶不漏水窗户能关人可以住进去。但你要在客厅加个书架、在厨房加个洗碗机、把卧室隔成两间就得砸墙、改管线每一次改动都可能把房子搞出问题。可维护性和易扩展性就是这间房子的装修设计。可维护性出问题了你能快速定位到是哪里坏了想改个逻辑你能明确知道改哪几行改完之后你不担心按下葫芦浮起瓢。易扩展性加新功能时你能尽量不动老代码就接上新模块像插排插一样插上就能用而不是把插排拆开重新接线。这两个属性不直接影响第一次的运行结果但它们决定这行代码今后每一周的维护成本。能跑只证明你现在成功了可维护易扩展决定你以后每次改动能不能继续成功。1.3 为什么初学者最容易踩这个坑其实代码能跑就行不是蠢而是某种必然。初学者阶段你身边的人、教程、考试题目评判标准几乎全是结果对不对。题目说出来你跑出正确结果就是满分。这种反馈机制会慢慢塑造你的判断代码好坏的唯一标准就是能不能出结果。而且还有一个心理因素——完成感。当你花了一晚上终于把程序调通了大脑会奖励你我搞定了的信号而此时让你回头拆函数、改变量名、梳理结构相当于在说你还没搞定这很反直觉。所以我特别理解为什么很多新手卡在这一步出不来不是不会写而是没有一个足够痛的场景逼你意识到能跑远远不够。2. 可维护性差是什么体验那些让你深夜崩溃的代码特征2.1 变量名和函数名的灵魂缺失我在代码评审里最常看到的问题就是命名。变量叫data、temp、result1、arr2函数叫deal、handle、test。这些名字在当时写的时候都非常顺手因为写代码的人脑子里清楚这个变量是什么。问题是代码过三个月你脑子里那幅图早就删了剩下的只有一个叫result1的变量和一个叫handle的函数。读代码的时候你只能靠猜。我自己早期写过一段爬虫代码变量名叫a、b、c函数叫get_something。某天网站改版我需要修改解析逻辑打开文件那一刻我整个人是懵的。最后我硬是花了两个多小时一行行往下读、往外推这行的a应该是指网页源码这个b是正则表达式匹配结果——那种感觉就像在考古而不是在改代码。从那以后我给变量的命名标准变得非常简单如果这个名字不能让我在五秒内想起它的含义那就说明名字起得不合格。好的命名长这样def load_student_scores(file_path): 从文件中读取学生成绩返回整数列表 with open(file_path, encodingutf-8) as f: return [int(line.strip()) for line in f if line.strip()]file_path、load_student_scores、f所有信息都写在名字里了。这不需要什么高深技巧只需要养成习惯。2.2 魔法数字与复制粘贴改一处漏三处的元凶魔法数字指的是代码里直接出现没有解释意义的数字。比如if student[score] 60: print(及格) if teacher[level] 2: print(高级教师)那个60是及格线那个2是教师级别阈值。今天看还能猜但如果是if value 0.863呢或者if count 7呢这些数字背后代表业务规则可你完全看不出来。正确做法是给它们起名字PASSING_SCORE 60 SENIOR_TEACHER_LEVEL 2 if student[score] PASSING_SCORE: print(及格) if teacher[level] SENIOR_TEACHER_LEVEL: print(高级教师)这样将来及格线调整到65你只需要改一处常量。而复制粘贴的问题更隐蔽你为了图省事把一段逻辑复制到了另一个文件后来你修复了一个bug但忘了同步到另一处。这类问题在真实项目里非常致命而且很难排查——因为代码都在但逻辑版本已经各不相同了。2.3 一个可维护代码应该长什么样拆解一段烂代码的改造过程我拿一个最常见的统计场景做例子。原始代码大概是这样的data open(scores.txt) sum 0 n 0 for i in data: sum int(i) n 1 print(sum) print(sum / n)能跑结果也对。但问题很多文件句柄没有关闭sum遮蔽了Python内置函数逻辑全堆在全局代码里没法复用更别提后续加功能了。我把它改造成这样def load_scores(file_path): 读取成绩文件返回成绩列表空行自动跳过 with open(file_path, encodingutf-8) as f: return [int(line.strip()) for line in f if line.strip()] def compute_stats(scores): 计算一组分数的总数、个数和平均值 total sum(scores) count len(scores) return { count: count, total: total, average: total / count if count else 0 } if __name__ __main__: scores load_scores(scores.txt) stats compute_stats(scores) print(stats)改动其实不大但每一行都有了意义load_scores管输入compute_stats管计算if __name__ __main__管入口。以后要加及格率不用碰这两块代码直接新写一个函数要换个数据源把load_scores里的文件读取换成数据库查询就行。这就是可维护性带来的红利——改局部不炸全局。2.4 维护性自查清单给代码做一次体检我给自己写代码时会用一个很简短的清单快速过一遍你也可以试试检查项合格标准常见问题命名不看注释也能猜出变量/函数用途data、temp、handle这种无名氏函数长度一眼能看完整个函数在做什么超过两三屏还不停直接警惕重复代码同类逻辑只出现一次同一段代码在两个文件里各一份魔法数字业务数字有常量名或注释裸奔的60、0.8、86400资源清理文件、连接等有始有终open了不closewith可以根治注释注释解释为什么而不是是什么每行注释都在翻译代码而不是补充背景这个清单不是用来装专业的而是当你凌晨两点被线上问题叫起来打开自己三个月前写的代码时能少点想哭的冲动。3. 易扩展性需求说变就变代码能不能跟上3.1 需求变更不是产品乱来而是世界本来就会变很多初学者对加需求有种怨气觉得是别人不懂代码才会提需求。但往深了想需求变化其实是常态。一开始做课程设计老师只要求统计总分后来要求按班级分组再后来要求画折线图。你做个人项目也是第一版只是抓数据后来你想加个定时任务再后来想加个可视化页面。这不是谁故意折腾你而是你对自己的业务理解也在不断加深需求自然跟着变。所以易扩展的本质不是预测未来而是为变化留个口子让新增东西不需要把老代码推倒重来。3.2 从改代码到加代码开闭原则的朴素理解软件设计里有个著名的开闭原则对扩展开放对修改关闭。听着特别玄乎翻译成大白话就是加一个新功能最好是往项目里加一段新代码而不是改旧代码的逻辑。举个实际的例子。你写了一个计算订单折扣的函数def calc_discount(amount, user_type): if user_type normal: return amount elif user_type vip: return amount * 0.9 elif user_type svip: return amount * 0.8过段时间产品说要加个企业会员打七五折你怎么办往函数里再elif一个elif user_type enterprise: return amount * 0.75这样能跑但每加一个用户类型都要改这个函数改多了以后这个函数就变成了又臭又长的分支判断而且你每次改动都可能误伤其他类型的逻辑。比较有扩展性的写法是把不同用户的折扣策略拆开class NormalDiscount: def calc(self, amount): return amount class VIPDiscount: def calc(self, amount): return amount * 0.9 class EnterpriseDiscount: def calc(self, amount): return amount * 0.75 DISCOUNT_MAP { normal: NormalDiscount(), vip: VIPDiscount(), enterprise: EnterpriseDiscount(), } def calc_discount(amount, user_type): strategy DISCOUNT_MAP.get(user_type, NormalDiscount()) return strategy.calc(amount)以后再加双十一会员写一个新类往DISCOUNT_MAP里注册一下调用方一行都不用改。这就是加代码而不是改代码。对初学者来说不一定要立刻掌握类、策略模式这些但可以先建立这个意识改动越少越好。频繁修改的代码迟早会出问题。3.3 三个最简单的扩展性设计手法抽象、参数化、分层抽象、参数化、分层这三个词是我觉得新手最容易上手也是性价比最高的扩展性手段。抽象就是提炼出共同的东西。比如你有三个地方都要计算两个日期之间隔了几天不要每次分别写一遍datetime计算逻辑而是封装成函数days_between(date1, date2)所有地方都调它。将来要改计算规则比如含不含当天你只需要改这一个函数。参数化是给代码留旋钮。比如你的爬虫需要设置请求间隔可以在代码开头定义REQUEST_INTERVAL 2而不是把time.sleep(2)散落在各处每次想调整都要全局搜索。参数化的核心是把容易变的东西从代码内部提到明面上改起来不用找、不用猜。分层是把不同职责的代码放在不同位置。最朴素的分层就是分成三个部分输入模块读文件、请求网页、业务模块计算、判断、处理、输出模块打印、写文件、入库。你不需要搭什么框架只要在写代码的时候有意识地把这三类逻辑分开就已经比大多数初学者强很多了。分层的最大好处是你的输入从文件改成数据库业务逻辑完全不用动你的输出从print改成写Excel输入和业务也不用动。3.4 过度设计的边界什么时候不要想太多讲完了这些手法我必须泼一盆冷水不是所有代码都要预留扩展点。我见过一些新手学了两天设计模式写个两行代码的函数还非要套一个类结果代码膨胀了好几倍解释成本远远大于收益。判断要不要留口子我一般问自己三个问题这个变化是真实需求还是我脑补出来的现在留口子的成本高不高高的话以后重构行不行如果以后需求真的来了我能不能在半小时内改好如果答案是现在用不到、改起来也不难那就直接先做好当下的事。所以这里有个很实在的建议对一次性脚本可以适度放松对要长期维护的项目宁可多想一步。纯粹套用设计模式制造复杂度跟纯粹能跑就行一样都不是好的状态。4. 那些能跑就行的代码后来都怎么样了4.1 典型场景一临时脚本变成长期维护的线上服务我见过太多人写脚本的心态是这个小脚本我就自己跑一次不用管那些。问题是很多临时脚本的命运是被反复使用今天跑一次下周又跑一次下个月换个参数再跑一次再后来同事看见了说你这个挺好用我也要用。于是这个当初随便写的脚本变成了事实上的长期工具。这时候当初没有函数、没有参数、没有异常处理的代码每被使用一次都是在累积风险。我接手过一个历史遗留脚本代码里没有任何错误处理只要某一行数据格式不对整个程序就崩。你要让它稳定运行几乎等于重写。而你完全可以在一开始用多写几行的代价换掉后面无数次的返工。4.2 典型场景二多人协作时的代码事故等到你实习或者参加工作你会发现代码不再是写给自己看的。团队里其他成员会读你的代码、改你的代码、用你的代码。这个时候能跑就行的代码简直就是协作事故的高发区。举一个特别常见的例子A同学写了一个处理数据的函数参数顺序是(origin_data, target_data)B同学没细看以为是(target_data, origin_data)调用的时候传反了。由于两个参数类型完全一样程序不报错但结果全错。排查这种bug相当耗时因为每一个地方看起来都挺对。应对方式也不复杂如果能用一个对象打包参数或者至少把参数名写清楚用关键字传参就能避免大部分这种问题。但很多能跑就行的代码根本不会考虑别人怎么读我的代码只考虑我这会儿怎么顺手怎么来。4.3 典型场景三面试和开源项目中的见光死还有两个场景是很多初学者没意识到的面试作品和开源项目。面试官看你的项目时真会逐行读代码。我听过不止一个面试官吐槽候选人的项目功能齐全但代码里全是test1、test2这种名字函数一个能有两百行。这样的代码一打开印象分基本就没了。面试官会想项目交付之后如果让你继续维护你是不是也会把这个风格带进公司代码库开源项目也一样。你的开源代码放在那里如果别人想用首先得看懂你的代码。可读性差的项目就算功能很强也会劝退大量潜在贡献者。于是你的项目永远只有你一个维护者而你忙不过来的那一天就是它停止维护的那一天。5. 给初学者的可落地改造路径从能跑走向能维护、能扩展5.1 第一步写完功能后先别急着提交做一次旁观者阅读我的习惯是当代码跑通之后先离开键盘深呼吸然后用一个陌生人的视角重新读一遍自己的代码。假装这是别人写的读的时候不断问这个变量是干嘛的这个函数为什么存在这一步在算什么如果哪个地方读不懂就说明那个地方需要改进。绝大多数新手遇到的问题其实就出在没有这个阅读环节——写完就跑跑了就忘。我强烈建议你至少保留一次旁观者阅读这个动作花不了几分钟但能把代码质量拉高一个明显的档次。5.2 第二步给函数减肥让每个函数只做一件事一个函数只做一件事听起来特别简单但我发现很多新手的函数干着干着就把它当成了一张杂货清单先读文件再清洗数据再统计最后画图全塞在一起。一旦函数超过一定长度理解和测试的难度就直线上升。我给一个可操作的衡量标准如果一个函数你需要上下滚动鼠标才能看完就该拆了。拆完再检查每个新函数都应该能用一个简单的句子说明白它做什么。比如读取文件计算平均分生成图表各司其职。这样做还有个额外好处——你以后测试代码的时候可以单独测计算平均分这个函数而不会被文件读取和画图的逻辑干扰。5.3 第三步为变化预留缝隙配置化与接口思维过去的我写程序永远是把变量写死在代码里。后来发现所有需要改的东西最好都集中到一个地方。比如要爬的URL、要过滤的关键字、要遍历的文件目录全部提到顶部或者独立的配置文件里。这样以后改动的时候你不需要在一堆逻辑里钻来钻去。接口思维则稍微进阶一点。简单说就是你的代码跟外部打交道的方式不要轻易变。比如你封装了一个fetch_weather(city)函数外面调用的人只关心传入城市、得到天气数据。只要这个输入输出约定稳定你内部哪怕把接口从免费API换成付费API调用方都不需要变。这种面向接口的思维就是易扩展的核心——让易变的东西待在边界内不要让变化传导到所有代码中。5.4 第四步用Git提交记录倒逼自己养成好习惯很多初学者刚开始用Git只知道commit不知道提交信息怎么写。我建议你从第一天开始就认真对待每一次commit每次只提交一个逻辑改动提交信息写清楚做了什么和为什么做。为什么这能倒逼代码质量因为你如果一次提交改了七八个文件每个文件里又是重构又是加功能提交信息根本没法写。你被迫思考这次提交的主题到底是什么。这种思考反过来会让你把改动拆小、拆干净。不瞒你说我现在回看自己早期的提交记录一条条信息简单得像黑话但后期逐渐规范之后翻历史代码定位问题就变成了一个高频但低痛的操作。6. 我的实操心得与额外建议6.1 哪种代码真的可以说能跑就行说了这么多我也要给能跑就行一个合法存在的空间。我自己的判断标准很简单丢掉的脚本可以不管。比如临时跑一个数据转换、写一个一天后就不再用的批处理命令这种代码再怎么能跑就行都没问题。但凡是第二天还会用到、要给别人看、要跑很多次的代码就得拿出可维护、易扩展的标准来。所以别误会我并不是逼你在所有代码上都追求工程化。我真正想说的是你至少得能分清哪些代码是一次性的哪些代码是需要长期养的。最怕的是把一次性代码当长期项目写或者反过来把长期项目当一次性代码写。6.2 重构旧代码的黄金步骤如果你手头已经有能跑就行的烂代码别急着删除重写——重写有风险而且可能把已经修好的边界情况弄坏。我建议按这个顺序来先补测试把当前代码的所有输入输出样例记录下来最好是写成自动化测试让它们成为你的安全网。没有测试你重构的每一步都在裸奔。小步重构每次只改一小块比如给一个变量改名、抽一个函数改完立刻跑一遍测试。不要一次改完再跑那样一出问题你根本不知道是哪一步弄坏的。保持行为不变重构的定义是不改变外部行为只改善内部结构。先追求结构变好再追求功能优化两件事不要混在一起做。等结构清晰了再扩展老代码里逻辑乱成一团的时候直接加新功能只会更乱。先把结构理顺扩展才谈得上。6.3 刻意练习刷题之外给自己加一道质量题很多人学编程非常努力刷算法题、看教程、抄项目但练的都是把功能做出来。我建议你在日常练习里给自己加一道质量题写完之后试着把代码往可维护和易扩展的方向改一版。比如你用快速排序实现了一个排序需求那不妨再想想——如果以后要按不同字段排序怎么设计函数参数如果要支持中文排序、数字排序、日期排序代码怎么组织这些多想的几步才是可维护和易扩展能力真正成长的地方。我自己的体会是早期写代码追求的是奇迹时刻——一段代码啪一下跑通了那种爽感无与伦比。可现在回头看真正让我少走弯路的并不是那些能跑的瞬间而是后面愿意停下来打磨结构的耐心。你写的每一段代码都像在给未来的自己留言写清楚一点未来的你就能轻松一点。如果你正在代码能跑就行这个阶段不用急也不用自责因为所有人都是这么过来的。你只需要在每次跑通了之后多问一句如果以后要改我会想在这里改吗从这个提问开始你已经在往更好的方向走了。
返回列表