ARTICLE DETAIL

资讯详情

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

别再把逻辑全塞进main函数:功能函数拆分与代码清晰布局实战

别再把逻辑全塞进main函数:功能函数拆分与代码清晰布局实战 1. 先说现象一段全是if的main函数是怎么把人逼疯的最近帮一个刚学编程的同事看代码发现一个特别典型的毛病半个程序都写在主函数里。功能函数倒是有但更像是把一堆变量塞给几个“工具人”main从头到尾贯穿所有细节改了密码规则要翻main加了字段也要翻main连想打印一行调试信息都得先想清楚该放在哪个if里面。今天这篇笔记想认真聊聊我理解的功能函数与主函数的布局——用最朴素的话说就是代码里谁该干什么、谁该站哪、怎么把一团乱麻理成一份一遍能看懂的清单。这篇笔记适合刚接触模块化编程、程序规模开始超过几百行的新人也适合那种“函数用了不少但总觉得代码差点意思”的朋友。1.1 一份“看起来能跑”的main函数长什么样这种写法在初学者项目里太常见了我先还原一个典型。一个控制台注册程序功能很简单用户输入用户名和密码程序做几项校验然后写入本地文件。很多人写出来的main会是这个形状def main(): print( 用户注册 ) username input(请输入用户名) password input(请输入密码) if len(password) 8: print(密码至少8位) return if username password: print(用户名和密码不能相同) return if password.isdigit(): print(密码不能全是数字) return if len(username) 3: print(用户名至少3个字符) return if in username: print(用户名不能包含空格) return if username.isdigit(): print(用户名不能全是数字) return try: with open(users.txt, a, encodingutf-8) as f: f.write(f{username}:{password}\n) except Exception as e: print(保存失败, e) return print(注册成功欢迎, username, 你的ID是, len(username) * 7)这段代码其实只有三十行左右还不算最夸张的版本但病根已经非常明显。校验规则、文件操作、成功提示全部黏在main身上读它的人被迫同时记住“密码至少8位”“用户名不能是纯数字”“写文件失败要怎么办”等十几条细节。这里每一段if被塞进去时都觉得很正常可一旦数量多起来入口函数的阅读负担就成倍增长。我见过上百行的main函数里面装着比这复杂得多的业务规则基本没人能在不画脑图的情况下一次讲清楚它做了哪些事。1.2 为什么这样写会让代码越来越难改把逻辑全堆在main里最大的问题不是“代码太长”而是“没有拆装性”。你想在注册成功后加一个“发送欢迎邮件”的动作就得回到main里在保存成功之后插一句想加一条“用户名不能重复”的规则还得回到main里在几个if之间再插一段。每插一次main就又高又壮一点到后来谁都不敢动它因为牵一发而动全身。这就像一个抽屉里既放证件又放零钱还放旧发票平时用着顺手真要找一个东西就得全倒出来翻。为什么新人很容易掉进这个坑我复盘过这个习惯的来源。刚学编程时接触的示例程序几乎都是从上到下一口气写到底的老师的重点往往放在语法和逻辑结果上没有人提醒“这一步其实是一段独立的职责”。加上初学时把功能函数理解成“可以被重复调用的代码块”而不是“把复杂度隔离开的容器”于是代码只有复制粘贴超过两次时才想到做函数遇到不重复的逻辑就顺手丢进main里。这种思路在小脚本阶段完全够用程序规模一旦涨到几百行修一个bug需要通读全函数时布局的意义才会真正暴露出来。布局从来不是排版好看它是让阅读代码的人脑子少装一点没必要的细节。2. 主函数的正确体重为什么我坚持让main保持在20行以内2.1 main函数只该管三件事入口函数不是写业务的地方它应该是整份程序的“目录页”。读者打开main一眼能看出程序先干什么、后干什么、出错了大概往哪走剩下的事情交给功能函数去展开。基于这个定位我习惯让main里只保留三类内容准备上下文、调用功能函数编排流程、统一收尾。准备上下文就是读配置、建连接、初始化全局状态这类动作它们本质上是为后面的步骤备好材料编排流程是核心把大任务拆成一个个功能函数然后按顺序调用统一收尾负责处理所有路径都会遇到的清理、汇总输出或者退出。这里可以做一个简单的归类判断代码到底该不该留在main里应该出现在main里应该拆成功能函数读配置、初始化连接等准备工作具体的数据校验、规则计算按顺序调用功能函数、判断调用结果超过十行的独立业务步骤对功能函数返回结果做简单分支可以被多处复用的逻辑程序收尾、统一输出汇总任何需要单独测试的行为我平时判断的标准就一句话如果这行代码抽成函数读main的人会不会更省力。会就抽如果只是机械地把三行代码换成一次函数调用那反而是形式主义。布局的价值是降低认知负担不是做给别人看的仪式。2.2 一个“标准体重”的main长什么样用前面那个注册程序来说我认可的main大概长这样def main(): config load_config(config.ini) username input(请输入用户名) password input(请输入密码) result validate_user(username, password) if not result.success: print(result.message) return save_user(username, password) print(build_welcome_message(username))这段代码没有告诉读者“密码不能全是数字”但读者根本不需要知道这些细节就能看懂流程加载配置拿输入校验保存打印结果。想改密码规则直接去validate_user对应的函数实现里改main这一层完全不用动想加“发邮件通知”只要在save_user后面补一行send_notification(username)main仍然保持清爽。这种“加需求时入口函数几乎不用改”的效果才是函数与主函数布局带来的真正杠杆。2.3 光短不够短得有节奏才叫布局把main压到二十行以内只是结果真正的关键在于节奏。我建议把main里的调用顺序当作一份流程文档来写初始化在前取输入、校验、存储、输出在后这个顺序本身就在讲故事。如果顺序乱了比如先保存再校验读代码的人会立刻感到别扭因为他脑中的业务逻辑被打乱了。还有一个容易忽略的细节入口函数的变量名尽量简短统一username、password这种直接用输入值别弄出user_1、temp这种临时感很强的名字。入口层变量越少越好最好只保留当前流程需要传递的关键数据这样后续扩展功能函数时传参也会自然变得清爽。3. 功能函数的拆解标准什么时候该拆、什么时候该留3.1 拆函数的五个信号拆函数这事难的不是会不会写函数而是有没有找到“该拆”的那个点。我总结过五个信号命中任何一个就值得认真考虑拆分。第一一段逻辑超过一屏。一屏大概三十到四十行超过这个范围人眼看不到函数全貌读起来必须来回翻错误率直线上升。第二同样的逻辑在两处以上重复。复制粘贴第三次出现时基本就该拆了。这类重复不只是代码丑的问题更危险的是以后改规则容易漏改某一处变成隐蔽bug。第三if嵌套超过三层。三层以上就会形成“代码香肠”一层套一层读代码的人得反复数括号和缩进改起来更是提心吊胆。第四局部变量超过十个。一个函数需要同时记住的状态太多大脑就超载了。第五函数名里出现“然后”比如“存数据然后发邮件然后记日志”。一个函数名里只要需要连词它多半就不止在干一件事。我特别想强调第五点。很多人拆函数时纠结的是“这段代码有多长”但真正有价值的判断依据是“一个函数里同时做了几件事”。校验密码的函数顺手把用户名校验了再顺手把用户存进文件这种函数无论多短都该拆。拆开之后你还会发现原本不可复用的缝隙里冒出了好几个可以单独使用的小零件。3.2 参数和返回值怎么定才算不过度设计拆函数最容易卡住的地方是接口设计。我的原则很简单参数能少就少超过三个就考虑用对象打包返回值尽量保持单一语义。举个例子计算订单折扣。直白的写法是calc_discount(price, user_level, coupon_code, vip_expire_date)四个参数排成一排调用方要记住它们的前后顺序将来再加一个参数所有调用处都要跟着改。更稳的做法是让调用方传一个order对象calc_discount(order)函数内部按需取字段。这样一来订单新增字段不影响接口稳定性也顺带逼着你要给数据画一个静态形状而不是让参数列表成为临时仓库。返回值方面新手容易在同一个函数里塞三件事返回结果、返回错误信息、顺手改一个全局变量。我建议从小项目阶段就养成一个习惯状态跟着结果走。像前面代码里的RegisterResult携带success和message调用方拿到它就能判断下一步怎么做。错误信息不要在功能函数内部用print直接输出打印是表现层的活校验函数只需要告诉上层“成不成、不成是什么原因”具体怎么展示交给main或页面层。3.3 有些代码真的不该拆拆多了同样是灾难。我见过有人把“密码长度不足”这种一行判断也单独做成函数整个文件里充斥着两到三行的微型函数阅读时要在十几个函数名之间来回跳跃。判断标准还是回到那五个信号既没超一屏、又没重复、又没深层缩进、又没有多个动词那就让它平铺着别自找麻烦。举一个极端的例子。load_config函数里要读取文件路径、数据库地址和日志级别本来三行顺序读下来非常清楚你非要拆成load_host、load_port、load_user三个函数读的人需要来回跳着拼凑信息意义就很低。拆分的真正目的是隔离变化点让每一块能被独立理解和测试。没有变化点的地方保持线性就是最好的布局。我有个做技术评审的朋友说过一句很有意思的话代码布局的目标是让一个普通水平的程序员也能在十分钟内改完需求而不犯大错。达不到这个效果拆多少函数都只是自我感动。4. 落地布局的操作细节命名、参数与返回值的统一规矩4.1 命名规则让函数自己解释自己布局如果只解决“放在哪”那还缺一半每个功能函数得有自己的“门牌号”。命名这关在小白阶段最容易被当成“随便起也没什么影响”但恰恰是它最影响长期可读性。我的习惯是动词加名词比如save_user、validate_password、build_welcome_message看到名字就知道这函数要做什么。返回布尔值的判断函数尽量用is_、has_、can_开头比如is_valid_username、has_admin_permission放进if条件里一读就是完整一句话。反面教材我见过很多handle1、do_thing、aaa、temp_function这种名字等于没有名字。为什么老程序员常说“命名是写代码时最难的事”因为名字一旦起得清楚函数的结构通常也会跟着清晰名字起得模糊往往说明你自己都没想清楚这个函数到底对谁负责。一个功能函数如果需要写三行注释才能解释清楚在干什么先别急着补注释回头看看是不是名字没起好或者这个函数本身就拆错了。函数名是最好的注释它应该让读代码的人无需跳进函数体就能推理主流程。4.2 文件内从上到下的布局顺序大型工程通常有包管理和模块划分我们暂时不往那个深度走先看一个单文件程序怎么排才顺。我手里一份写得舒服的代码文件阅读顺序几乎和依赖顺序一致导入区在最上方接着是常量定义然后是工具函数或辅助函数中段是核心业务功能函数最后才是main入口。这个顺序背后的逻辑是你从上往下读时每读到一个函数它依赖的“零件”已经在你脑子里了不需要频繁跳回文档上方查定义。我习惯把相关函数放成一组。所有和校验相关的validate_xxx挨在一起所有存储相关的save_xxx、load_xxx挨在一起。私有辅助函数用下划线前缀比如_read_users()表示“这个函数不是给模块外部用的”别人看到下划线打头就知道不要直接调用它。main放在整个文件的末尾它调用前面所有函数这样文件本身就是一张微缩的代码地图从下往上读就是从一个大业务流程一路拆到最小零件的过程。4.3 注释写“为什么”不写“是什么”很多刚走出“函数全塞main”阶段的朋友对注释的理解还是“用中文把代码复述一遍”。我见过考生式注释“# 这里判断用户名是否为空”但这行代码本身就是if username 注释完全没有提供新信息只增加了阅读量。我真正需要的注释是解释代码里看不见的上下文也就是为什么必须这么写。例如“# 用户名最长20字符避免与其他系统对接时触发长度限制”读到的人才知道这条规则是外部约束不是随手定的。函数级注释我建议写docstring风格用两三行说明输入是什么、返回什么、什么情况下会抛异常。这不是给机器看的形式是给三个月后的自己留的记忆卡。一个朴素标准足够检验注释好坏注释的价值等于信息量减去重复度。只会复述代码的注释是负资产它会像噪音一样干扰后续维护者。每次写完注释我都习惯先问一句这句注释提供了哪些代码本身没有的信息回答不上来就删掉。5. 一个重构实例从60行main函数到清爽布局的全过程5.1 还原需求一个带校验和持久化的注册程序为了讲清楚布局的过程我拿一个现实中很常见的小需求来练手控制台注册程序。需求是用户名长度3到12位、不能包含空格密码至少8位、不能和用户名相同、不能是纯数字注册信息写入本地文件成功后打印欢迎语和ID。这个功能不复杂但天然包含输入、校验、存储、输出四类典型职责对应的正是四种常见功能函数非常适合拿来练布局手感。5.2 初版全部堆进main的代码初版结构我在第一节已经展示了main里一边收集用户名和密码一边顺序写几个校验if每个失败就print一句并return接着try打开文件写入最后print成功消息。这个版本是典型的“未经布局”的代码问题非常集中校验规则、持久化逻辑、展示逻辑互相纠缠改任何一处都得先顺着if一个个排查。更麻烦的是一旦想写单元测试根本无从下手因为所有状态都活在main的局部变量里没法单独把“密码校验”摘出来测试。这种代码如果只是自己跑着玩确实没什么大错。但一旦程序要继续加需求比如增加邮箱注册、密码找回、用户重复检测main就会迅速突破两百行到那时每一次改动都像在迷宫里打转。重构的意义不是让代码显得高级而是给未来的改动预留稳定且清晰的开口。5.3 拆分过程把职责归类再让main只剩调度重构第一步不是写代码而是划分职责清单。我把程序的需求拆成四类校验用户名、校验密码、保存用户数据、生成欢迎消息。然后为校验设计一个返回对象RegisterResult让校验函数不但返回成功与否还自带失败原因。这样main不需要用一堆状态值去推断错误原因。完整代码可以这样组织class RegisterResult: def __init__(self, success, message): self.success success self.message message def validate_username(username): if len(username) 3 or len(username) 12: return RegisterResult(False, 用户名长度需为3-12个字符) if in username: return RegisterResult(False, 用户名不能包含空格) return RegisterResult(True, ) def validate_password(password, username): if len(password) 8: return RegisterResult(False, 密码至少8位) if password username: return RegisterResult(False, 用户名和密码不能相同) if password.isdigit(): return RegisterResult(False, 密码不能全是数字) return RegisterResult(True, ) def validate_user(username, password): result validate_username(username) if not result.success: return result return validate_password(password, username) def save_user(username, password): with open(users.txt, a, encodingutf-8) as f: f.write(f{username}:{password}\n) def build_welcome_message(username): user_id len(username) * 7 return f注册成功{username}你的ID是{user_id} def main(): username input(请输入用户名) password input(请输入密码) result validate_user(username, password) if not result.success: print(result.message) return save_user(username, password) print(build_welcome_message(username)) if __name__ __main__: main()拆成这个结构之后有几个设计细节值得展开说说。把校验拆成validate_username和validate_password而不是合成一个大函数是因为这两类规则的变化频率可能不一样以后改用户名规则时不会牵连密码逻辑。用RegisterResult对象而不是返回True或False是因为main拿到结果后可以直接读result.message否则就得在每个校验函数里直接print表现逻辑就会再次跑回业务函数内部。save_user单独存在意味着以后从文件存储换成数据库只需要改这个函数的实现校验和入口都不用动。build_welcome_message单独成函数输出格式随时能调不影响存储和校验。5.4 复盘重构后最容易把布局改回去的三个瞬间重构完成并不是终点。我在代码评审中见过最多的场景是过了一段时间main又悄悄长回来了。第一次常见复发是新增“用户名不能重复”规则时有人直接在main里补一个if去遍历用户文件。正确做法应该是由save_user或单独拆一个find_user函数负责查重main仍然只做调度。第二次常见复发是改欢迎语格式时有人直接在build_welcome_message里加输入逻辑这属于“取输入”的职责应该留在main入口层。所以复盘时要给自己立一条硬规矩main这一层只做调度不生成新规则不实现细节。任何“新业务规则”都要进功能函数“新输入动作”都要留在入口层。守住这条线布局才不会过一个季度就被临时补丁堆回了原形。很多人以为布局是一次性重构实际上它是一个需要持续维护的习惯每次提交代码前多看一眼main函数有没有变胖就能省下未来大量的排查时间。6. 布局里的翻车现场过度设计、全局变量和顺序感缺失6.1 过度拆分函数只有两行main变成了空壳代码布局走到另一个极端同样可怕。我见过一位同事把每个if条件都抽成函数一个函数只有一到三行main最后只剩下“调用a调用b调用c”的空转状态。表面上每个函数都很短读起来却比之前更难因为读者要在十几个函数名之间来回跳连“密码长度不足6位”这种一眼能看懂的判断都要点进函数体里确认。我的判断标准依然没变函数名和函数体是否构成一个有业务含义的单元。单独拆一个validate_length(password)出来业务含义并不比一行if更清晰变化概率也不高拆它就是给代码灌水。真正好的拆分是你只需要看函数名就能准确推测它在流程中的位置不需要点进去验证。拆过头的时候你恰恰必须看函数体才能猜出这个名字到底在做什么。两种状态的分界线很细但写多了自然有手感。6.2 全局变量功能函数之间偷偷传数据的坏味道布局问题有时候不完全是“放哪”还有“数据从哪里来”。新手最容易踩的一个坑是在模块顶部放一堆全局变量所有功能函数直接读写。比如register.py顶部写一个current_user None后面的函数往里塞值。表面看代码短了不少副作用却十分隐蔽函数之间的依赖变成了隐性先后关系。你必须保证先调用A再调用B否则current_user还是空的而这种先后约定如果超过三四个函数谁都记不住最后只能靠猜。我的建议是小程序也尽量用参数和返回值传数据把全局变量当作函数之间的通信渠道是坏味道。全局常量和配置类数据例外例如MAX_USERNAME_LENGTH这种只读设定放外面完全合理。真正需要可变的全局状态时优先用类或对象把状态包起来通过方法调用传递和修改而不是让每个函数都伸一只手去改全局。用大白话说数据从哪进、从哪出要让人一眼看清不要从侧门偷运。6.3 工具函数与业务函数混放从上到下读代码时卡壳最后一个翻车点同样常见整个文件里工具函数和业务函数乱序混排。有人把validate_email这种通用工具写在main前面过两天加了一个send_email函数顺手插在后面再过两天又塞一个日期格式化函数整个文件前半部分变成公共工具杂货铺读者在主流程和工具区之间来回跳阅读体验全靠翻页硬撑。问题出在工具函数通常是“被依赖方”业务函数是“依赖方”。如果工具函数全部堆在文件开头业务函数放在后面倒还算清晰但多数人是“想到哪写到哪”哪个函数先写出来就放哪个位置。我的经验做法是和当前业务直接相关的辅助函数跟着业务函数走同组放在一起完全通用、将来多个模块都可能调用的函数才单独放进公共工具区域而且等真的出现第二个调用方之后再做提取也不迟。布局要优先服务当下读代码的那个人而不是为“未来可能用到的复用”提前铺路。在这些翻车现场中最伤结构感的永远是那句“先这样吧以后再说”。布局这事不会在运行时立刻报错但它会在三个月后某个加班排查问题的深夜以“我怎么读不懂自己写的代码”的方式惩罚你。我自己的体会是把main函数当目录页维护把每个功能函数当一篇文章的章节整份代码就不再是需要一次性吞下的长篇而是一本能随手翻到特定页码的文档。从“写完能跑”到“写完能读”的转变才是功能函数与主函数布局最值得琢磨的部分。
返回列表