写代码如同打扫屋子, 有句话叫一屋不扫何以扫天下. 如果单个的一个模块代码都不能管好, 如何成就一个完善的软件系统? 今天我们来说说, 一个代码模块的代码是如何一步步腐化变质, 到最后程序员都不愿意去维护它, 然后要么重构, 要么废弃换新模块的?
代码是有一定的周期的, 这个没有错. 为什么有的代码跑上几十年任然好用, 而现在互联网公司的很多代码, 每年都要做好几次重构? 一个成立 2 年的互联网公司, 做一个支付系统, 可以做了 4-5 代, 每次重构, 这样的代价有多大? 如何才能让原有的代码生命周期更加长, 而不增加很多的学习维护成本, 开发一次使用更久呢?
大部分程序员是没有很多机会从 0 开始搭建一个新程序的, 更多的时候是接手别人写的代码. 有代码移交还好一点, 往往因为各种因素, 这些因素你懂的, 没有产品文档, 没有设计文档, 没有程序说明, 程序里可能连注释都没有. 然后, 程序员更新换代又极其的快, 互联网时代, 程序员在一个公司的平均年资也就 1 年多, 程序就又被传给下一任维护者. 很大可能的情况是, 最终到你手里的程序各种问题, 却能实现基本的功能需求, 但代码内部各种问题让程序员总有一个冲动, 重构它. 今天不想说重构的问题, 而是从根源角度分析, 程序为什么会变成这个样子?
什么是程序的腐化?
什么是一个软件的质量? 一个分类标准是软件外部质量与软件内部质量的统一, 外部质量是对外表现是否正常, 内部质量是对后续开发有没有坑, 就是我在这里说的软件有没有腐化. 内部质量标准有: 可维护性, 灵活性, 可移植性, 可重用性, 可测试性, 可理解性(摘录自代码大全). 不符合以上标准都可以称之为代码腐化, 形象的理解就是一个苹果, 从内部开始烂了, 烂到原本应该负责内部代码的程序员拒绝去维护了.
实际的代码腐化的例子:
代码混乱, 没有代码规范不该连数据库的模块连了数据库模块间的调用混乱: 模块内部的调用混乱, 例如 C# 代码已经使用了 EntityFramework, 代码中跳过 EntityFramework, 直接用数据库连接修改数据. 框架与其他不一致, 不统一: 有的包管理使用 gradle 管理, 有的使用 maven. 有的后台用. Net, 有的用 Node, 有的用 Java.
用了 HttpClient, 又使用 Feign 去连接其他应用模块有一些设计前后不一致: 有的代码使用了统一的错误定义 CommonException, 有的用原生的 Exception. 微服务模块, 有 resource 层接口, 定义访问的路径, resource 的 Impl,service 的接口提供具体的数据接口, serviceImpl 提供具体数据获取的实现. 而在具体编码时, 将大量的业务逻辑写入了 resource 的实现中.
太复杂的抽象不能做方便的变更: 一开始设计的 Job 系统, 上面是 2-3 张图片, 下面是动态生成的问题. 代码层面对于此设计做了很细致的抽象. 突然产品提出了某一个 Job 的图片有特别, 要求显示 10 张图片, 就对抽象的图片部分做了 if-else 的处理...... 无用代码, 废弃的接口没有标明
代码腐化的原因
没有代码会是 init commit 的时候就开始腐化的, 腐化都是循序渐进的, 要一个过程. 我总结了一些代码腐化的原因:
没有统一标准, 或者没有严格执行
统一标准之代码规范
每个程序员都是有自己的审美的, 例如即使是缩进长度这种代码里不影响任何功能的东西, 有的喜欢空 4 格, 有的喜欢 2 格. 有的喜欢黑色的编程背景, 有的喜欢白色的编程背景. 有的喜欢 if 后直接跟上左括号, 有的就喜欢另起一行. 代码规范还是要有的, 包含各种格式定义, 大小写规范, 命名规范等. 前端有各种 lint 工具 (jslint,tslint) 可以帮助规范, 后台的 ide 也有一些方法帮助. 像 Baidu,Google 这样的公司还有构建时的自开发的检查工具, 所以常常一个资深程序员第一次开发的代码要花上 1-2 天才能提交通过. 代码规范的混乱, 直接导致代码可读性的降低. 可读性直接影响后续的生产力. 一个程序员天天对着看不顺眼的代码, 怎么可能高效?
统一标准之基础规范
除了代码规范外, 项目命名, 通讯方式, 基本的程序框架, 后端 Java 的 springboot,sprintMVC, 前端的 angular,vue,react 等都需要统一, 还有统一的基础环境(eureka,elk,redis,apigateway 等). 不统一的后果是各种部署, 管理, 编码的低效. 例如搭一个 jenkins, 然后部署服务 A 用的 Maven, 服务 B 用的 gradle, 就导致编译代码写 2 套, 如果写一套基本一样的, 当然会快一些. 我统计的 java 代码中可以统一的部分(包含但不限于)Http 调用格式, 统一用 content-type:application/json,response 也统一要求这样. HttpClient 的标准化框架, 如 SpringBoot 项目管理工具: Maven,Gradle 项目的 CI,CD 配置管理模式, 例如统一成一个配置文件 application.properties 环境变量配置方式, qa,stag,prod. 不要有的人写 stage,staging, 也不要写成 production 等等细节代码基础结构: 例如标准的 maven 目录的结构
项目命名方式: com.(公司名).(开发组名).(系统名).(模块名)例如: com.omniprimeinc.cosmetic.application.server;Restful 接口设计统一: 大小写, 命名方式, Body 的最大大小例如, Post 接口是否可以加 PathParameter 和 QueryParameter.Post 接口是否可以不带 Body. 其他配套功能的统一性: 调用链, 动态配置管理, 缓存, 分布式事物数据库的统一: 统一数据库, 数据库版本, 是否可以使用存储过程等. 关于数据库统一性不在这里展开, 这点也非常的重要.
统一规范之公司统一框架
刚才说的统一, 很多是从公司层面的统一, 如果大家都只用 springboot, 都沿用统一的后端框架, 前端统一用 angular. 那么这个时候, 为了方便统一, 就需要有代码相关的脚手架工具, 直接生成基本的统一项. 这样一个工具的好处是可以直接一键完成许多基础工作, 并完成了底层的统一工作.
多头维护
代码腐化的一个很重要的因素是多头维护, 甚至是多代维护. 一个公共项目, 多个开发团队都在维护, 那就很难统一标准. 初始版本有一个架构, 然后换了一个架构, 开发更是换了几批. 人多手杂说的就是这样的情况. 任何开发团队接手一个旧项目时, 其实都是有学习和适应的成本的. 频繁的变更开发人员带来的坏处就是反复的人为制造这个成本, 其次就是有几率丢失之前的一部分标准和架构规划.
架构没有落地
代码模块的功能设计规划, 制定的标准, 没有详细的落地. 架构定了一套, 开发没有严格执行. 每天写代码这么忙, 架构只管架构, 不管细节. 开发每天撸代码, 只管功能, 不管架构和代码质量(这个质量不是指功能实现上的质量, 而是说严格执行各项统一标准的程度). 甚至说, 一个高层服务, 不能调用同级服务, 只能调用底层服务. 因为开发的没有严格执行, 甚至加了数据库连接, 直接去取了数据库, 这样的事一旦开了口子, 就像黄河决堤, 不可收拾了. 所以, 以上说的是架构的落地落实很重要, 让所有具体的开发参与者落实同一个标准. 架构就需要落实相关的设计, 相关的文档, 相应的执行检查. 现实的情况从来没有靠文档解决一切问题的, 可它能解决 80% 的问题, 另外就尽量减少开发人员的变动, 以减小换人带来的代码腐化问题.
防止代码腐化的建议:
代码规范标准化, 统一化治理
代码的内部质量其实很难保证, 规范执行也更多的靠人治, 甚至个别标准化的东西, 只能通过代码层面去检验, 无法通过测试或其他手段进行. 另外, 虽然有一些通行的默认标准, 更多的标准是代码的负责人自行确定的标准, 完全根据喜好来, 就像前面说的缩进的长度. 好比是老妈和丈母娘都跑来你家里帮你打扫卫生, 老妈喜欢把厨房里的锅都一路洗好挂起来, 丈母娘喜欢找一个橱柜, 都放在橱柜里, 你能怎么办? 没关系, 只要确定下一套标准, 不要经常改就好了.
架构严格落地
像前面说的标准, 特别是自定义的标准, 都需要落地. 第一优先文档, 第二是团队内部达成共识, 第三是执行.
严格的 codereview
防代码的腐化执行是一个关键点, 这个关键点就是 codereview. 在这个时间点, 再次与开发强调标准的重要性, 让开发知晓执行, 让测试监督, 让架构严格检查. 对于不符合的, 开发要再次去学习执行标准.
减少开发人员的变动
如果一个团队内, 原本标准就是统一的, 团队内实行敏捷开发, 任何一个开发都可以替代其他开发工作, 那么两个人交换任务就没有问题. 如果团队内都不统一, 这个变动就会严重影响开发, 代码腐化的可能会变的很大. 而团队间, 标准统一的可能性比在团队内更难, 要避免团队间的项目移交才是最优的方法.
代码模块架构 Keep it Simple&Stupid, 使用一眼就明白的架构
现实世界中, 业务需求永远的跑在技术需求前面, 很可能架构相关的设计没有文档, 没有说明, 一旦架构师不在, 原来的开发换人了, 导致原先的标准和设计无法继续下去, 一旦有交接, 标准的丢失和架构的变化不可避免. 这时, 此条原则就能发挥作用. 要使用傻瓜化的架构, 就是任何一个新来的程序员, 一眼就能看明白的架构. 例如使用公司统一的项目代码生成器, 脚手架, 生成统一的代码结构, 不需要程序员再投入代码结构学习的成本. 而且通用架构就意味着, 后续的架构师接受原先设计的可能性大增.
分层的概念很早就提出来了, 为什么 MVC 的概念会这么受欢迎? 我觉得是因为它足够傻瓜化. 数据库的架构结构中, 定义 repository,service 接口, serviceImpl 的实现等也成为了很通用性的设计. 有一个挺经典的例子就是 asp.net.MVC 的程序组织架构三个文件夹: Controllers,Models,Views 直接约定, 并强调约定大于配置. 只要是开发过这类程序的程序员, 都知道从 Controller 看路径和 RestApi 入口, Models 看数据结构, View 看 html 视图. 就不需要额外的学习成本. Maven 的标准目录结构也是一个例子.(见上图)
定期清理维护
如同像打扫房间一样, 一段时间不打扫, 自然会有边边角角的脏东西出来. 那么有没有定期的去清理呢? 还是不管他, 每次看到地上的一团纸, 都绕过, 而不是去扔了? 定期的清理维护不单是一个维护代码的过程, 更是一个重新梳理和统一标准的过程, 让现有的开发和架构, 再次的达成一致, 以提高战斗力
防止代码膨胀
微服务概念的提出后, 很好的解决了一个问题, 我们的一个代码模块应该写多少大的问题? 一个模糊的建议是一个 Sprint 能重写的大小, 如果更大, 就应该要差分. 有时候代码的清理维护工作也要以这个原则来处理, 不能出现过大的代码模块. 因为过大的代码模块, 首先带来的是程序的复杂度, 让程序员理解起来要更多成本. 其次, 模块内部的耦合度必然也提高了, 增加了难度. 这时, 需要的是切分出一个新模块出来.
总结
代码腐化是程序开发的一个经典问题. 代码内部质量的降低, 外部质量确可以被客户接受. 程序员经常想着, 等业务没那么忙的时候, 做点清理, 做点模块局部的微重构, 我的经验告诉你, 这些都是假的. 如果定下了定期清理, 定期检查拆分, 就必须立马去做. 做这些事的优先级要远高于业务需求. 为什么呢? 想象一个程序 C++ 的 printf, 里面的代码写的非常的烂, 变量命名都是 p,m,k 完全不能直观理解含义, 我们需要郑重的去处理这个模块的代码腐化问题吗?
其实不需要, 因为 printf 模块的代码需求, 就是把输入的内容打印出来, 永远都是这样, 没有任何新需求, 没有新需求就代表没有程序员需要深入内部去改它的代码, 不需要改的代码, 我们是没有动力去解决它的腐化问题的. 有必要经常深入解决腐化问题的代码, 必然是业务需求很多, 经常要变更的代码. 不要等到它已经腐化到代码生命周期都快走到头了, 才想起来去维护清理它, 要经常维修一下, 才能更好的让它发挥作用, 是吧? 一辆不打算开的老爷车, 发动机坏了就坏了, 一辆经常开的二手奔驰, 定期维护少不了.
来源: http://developer.51cto.com/art/201805/574790.htm