程序员的困境

杨旭 bio photo By 杨旭

程序员的自我救赎

作为程序员,我们经常身处这样的困境中

程序员的世界很单纯,写新的代码、或是修改旧的代码。对我们来说最直接的混乱是什么?

  • 创造新事物的时候,总是在茫茫文件中寻找以前是怎么做的
  • 修复缺陷的时候,总是在揣摩着代码内心深处的想法,为什么让她喝热水的时候,她会跟我生气
  • 我们每天由多少时间是用来而不是,多少次在阅读代码的时候,内心温习着我们知道的所有脏话
  • 混乱也许包含着超负荷的工作安排、变化多端的需求、经常死机的电脑等等突入其来的状况,但是对我们来说,最多的还是一坨肮脏的代码:凌乱的文件结构、纠结到死的逻辑、晦涩难懂的表达等等。我们总是找不到,或者很难找到我们想要的

这些肮脏的代码又是来源于哪里?

  • 所有东西都是由我们自己编写的,作为程序员,我们是一切的起源。也许是自己、同伴、或者是经常背锅的前任,终归是我们这群猴子

  • 无论是无论是主观问题还是客观原因,迫于压力还是心存侥幸,我们总是在制造混乱

为什么开发者会制造混乱?

抛开个人技能或者经验的问题的讨论,作为技术人员,终归是要不断学习和提升自己。 实际上,每个层级的人都在做着与自己能力匹配或略高于自身能力的工作,但是实际上,每个层级的开发者都经常在制造混乱。 每个人都有过类似的经历,在回顾自己的工作产出时,都会发现,自己其实可以做的更好,我们经常将原因归结于压力太大,没有时间,并且认为这是由于是需求太多历史缺陷所造成的。 所以,我们常常把希望寄托在需求变少上,让我们有时间清空缺陷,然后在整顿混乱,与其花时间讨论这个问题,我们不如去聊聊中国足球,至少我们还能选择看哪场比赛。

  • 不要以为特朗普当选了美国总统,就真的一切皆有可能
  • 现实点说,如果需求真的变少,最大的可能是:我们的收入比压力降的更快

与其被动的承受这个循环的无尽折磨,或者把希望寄托在这些不可能的事情上,不如从我们自身,或者说从我们所能控制的事情上寻找逃生通道。

实际情况中,面对交付压力和项目延期,通常使用的解决方式有两种:

  • 给现有成员更大的压力
  • 加入更多的成员

通过者两种方式,真的能打破这个循环么? 就如我们之前所说,在个体技能没有明显提升的情况下,增加压力只会进一步加剧混乱。虽然眼前的问题解决了,但是从长远来看得不偿失。 而增加成员呢,想必大家都有切身的体会,如果为了短期利益而增加成员,在眼下会造成更多的混乱,除非有健全的培养体系和长远的人力储备计划,但实际情况恰恰相反。

在这样的怪圈里面,混乱越积累越多,最终成为一锅黏糊糊的沥青,拖垮整个团队

也许我们尚且无力改变周围的环境,无法拒绝需求的涌入,无法指望NPC降临将我们从苦难中解决出来。但是与其被动的承受和喋喋不休的抱怨,不如在我们能力范围内想办法自我解救。

暂且不要将希望完全寄托在高深的技能先进的开发理念

  • 依赖注入和控制反转帮助我们构建低耦合的业务系统
  • 设计模式是良好的解决方案,帮助我们灵活的应对变化
  • Angular的组件化帮助我们化整为零、各个击破
  • React的单向数据流让我们能够灵活而且优雅的管理业务模型
  • 自动化测试帮助我们探索未知的城堡,保护我们已有财产
  • 微服务很具诱惑,让我们轻装上阵,并且专心致志
  • 敏捷和DevOps为我们创造了”乌托邦”一样的世界,一切都是那么灵活和轻松

但是现实是,对于学习新技能、应用新技术、改善组织架构和工作环境,我们总是有一个非常“正当”的理由,我们没有时间,而且源自于我们自身创造的混乱

也许留给我们的出路,只有用最低的成本提高代码的质量,最大程度的改善我们的生存环境,我们自身才有余力去学习先进的技能,我们身处的团队才有余地去改善环境。

在我们日常工作中,什么是最不值得浪费的时间?

日常工作中,我们花费最多的时间是用来阅读代码;同样的,这也是最不应该浪费的时间,尤其是命名可以轻易的理解意图,却因为编码质量差,导致我们被误导、猜测、陷入到不必要的复杂度当中时。

如果从我们内部的因素来分析这些问题的来源呢?

也许源自于我们过度的自信不自信

1.自信

我们经常毫无理由的充满自信:

  • 能够一次性解决问题,加上这句就万事大吉
  • 需求不会发生变更,我不需要再来重新阅读和扩展
  • 不会产生缺陷,我不需要再来重新阅读和理解这段东西 总是觉得自己写好的东西不需要反复调试不会再重新阅读和修改,总是觉得能够运行就已经足够了。 可实际情况是,满足这三条场景的,只有没人使用的东西,静静的躺在仓库的角落,直到被人删除。 代码总是被人反复阅读,被自己或他人,其频率远远大于编写或者修改,并且它只是偶尔被计算机执行,更多的是面向人

2.不自信

我们总是在担心:

  • 不能按时完成需求交付或者缺陷的清理
  • 清理混乱会引入新的问题
  • 随便进别人家打扫地面会被赶出来 我们常常急于编码、运行,并且开始下一个任务,无论是为了尽快交付需求还是清空缺陷,或者是为了腾出时间学习新的知识。对于混乱常常妥协推卸或者视而不见

3.破窗效应

一幢有少许破窗的建筑为例,如果那些窗不被修理好,可能将会有破坏者破坏更多的窗户。 一面墙,如果出现一些涂鸦没有被清洗掉,很快的,墙上就布满了乱七八糟、不堪入目的东西。 一条人行道有些许纸屑,不久后就会有更多垃圾,最终人们会视若理所当然地将垃圾顺手丢弃在地上。 这个现象,就是犯罪心理学中的破窗效应!

因为历史问题,我们的系统中充斥这随意的代码和凌乱的设计,这往往导致我们对混乱视而不见并且轻易妥协,并最终进一步加剧混乱。

从哪里开始?

首先,不要急不要心存侥幸,像乌鸦一样一颗一颗的丢石子

从实际经验得到的教训:

  • 做的越快,死的越惨
  • 凡是心存侥幸,总是会打脸

无欲速,无见小利。欲速则不达;见小利,则大事不成。 —— 《论语·子路》

其次,改善编码质量的意义在哪里?

  • 从自身来说不是为了达成指标或者彰显个人能力,只为为了自救
  • 对他人而言,因为阅读并理解肮脏代码耗费时间,导致降低自身的工作产出,直接影响到个人成长和经济收入;或者因此占用了本该陪伴家人的时间,在办公室内通宵调试。不负责任的代码罪大恶极

第三,我们始终强调低成本,不使用任何有技术门槛,或者统一整改的方式,还是那个原因,我们“没有时间”。

第四,后面提到的很多方式方法,都是来源于流传多年的经典著作,只是根据性价比摘取简单的实践方案。这里仅仅只是开始,而不是我们的最终目标

命名

名称是索引,是我们探索的路标

通过命名来找到我们的目标,很类似通过“问路”来到达指定地点。

描述性(最为重要)

起名的目的,是为了让人理解,这里真的不需要过份的简洁。

就像你询问我如何找到宝藏,得到的答案却是:右、左、绕、左

我为了节省些口水,却把你丢在茫茫的大海里晕头转向。

如果我的答案是:右转直行,5海里后左转、绕过小山、在海岛左转,你还会在路上耽误大量的时间去猜测和探索么?

使用模糊的命名,根源只有一个:

哪怕查词典一个个单次累上去,也能为自己、为后来者提供极有帮助的线索。

名称不怕长,floorIndexOfTheBuilding一定好过f,如果有必要,即使把名称写成一篇抒情散文,我们也愿意认真的阅读。

切记 - 不要使用拼音缩写或者单次的首字母缩写

分享一个刚工作时的经历,进入公司接手一个老项目的维护工作,在阅读源码的时候遇到一个神奇的变量cwkpzz,历经千辛万苦才从一位前辈口中得知,这是财务开票制作

标准化

使用大家约定俗称的名称,不要在起名字的时候标新立异。

问路的时候,你希望听到方言么?

无歧义

谣言猛于虎

你明明告诉我走到“摩天大楼”左转,实际上那就是个“违建的二层民房”。

多少人被一个错误的名称引入深山老林,艰苦跋涉后才恍然大悟。

名称长度

名称的长度取决于他的作用范围

  • 在简单的循环里,一个i已经足够
  • 在一个类里面,id已经很清晰
  • 在一个系统里面,nameOfTheUser才足够明确

对于非静态类型语言,IDE难以提供精确的定位,可搜索的长名称可以帮助你快速和准确的定位

  • 淘宝的收件地址只填写A2栋309,足以证明你是真正的土豪

方法名

方法是用来做事的,动词+名次才能让人理解意图。

指路的时候告诉你过河这样你才知道怎么做,如果我只说一个,是应该潜下去还是喝干它?

如果能够进一步告诉你乘船过河,是不是可以避免你尝试游过去?

长度的游戏

有没有过这样的体验:

  • 打开一个几千行的文件导致编辑器卡死
  • 查看一个方法的时候上下翻页,看了后面忘了前面,想起前面找不到后面
  • 调用的方法拥有十几个参数,两根手指同步移动才能辨别谁是谁

过长的东西往往伴随着复杂和理解上的困难。我们生来畏惧困难,对于这类东西经常望而却步;即使硬着头皮去阅读和理解,在这个过程中也会耗尽耐心和精力

游戏规则

  • 方法的长度 < 30行
  • 文件的长度 < 500行
  • 参数的长度 < 4个

开荒攻略

  • for/if/else/try/catch这些带有大括号是最明显的提取点
  • 解析参数 - 执行操作 - 封装结果是最常见的划分顺序
  • 重复利用IDE的提取方法功能
  • 多于3个参数就提取为参数对象
  • 如果每个方法都足够短小,在文件超长的时候,才能快速、准确的拎出来一个新的文件

通关奖励

  • 短小的东西都是简单
  • 自带单一职责光环
  • 即使不使用任何技巧或者模式,看起来也是“高x格”的
  • 整洁而且有序,便于发现和应用各种模式

顺序

我们习惯于固定的顺序,无论是生活、工作、或是其他。 在习惯的顺序中,我们会更加的游刃有余。

先总体、后细节

我们在理解事物的时候,已经习惯于先总体、后细节,这也是我们大脑的正常运转方式,我们不应该跟自己过不去。 就像我们在刷朋友圈的时候,一定是先看标题,再决定是否点进去看细节。如果反过来呢,浪费大把时间,漫无目的的阅读文字和图片,绞尽脑汁的猜测,最后才知道他只是想炫耀“自己刚买了个新手机”。

阅读程序也是一样,总体概念在前、细节逐步展开,写代码不是在写悬疑小说,铺开一堆细节吊足观众胃口,最后才把谜底揭开。

凑热闹

相关的、类似的东西放在一起,总是能给我们带来便捷

想想我们出去办个业务有多难,今天去提个申请,明天去开个健在证明,隔几天在跑到第三个地方提交一堆材料……,最后才能在一个从没去过的地方办好自己的事情。 大量的时间用来寻找下一站、赶路和排队,每一次都是刻骨铭心的体验。

阅读程序也是一样,如果相关的东西总是四分五裂,总是在不停的寻找、跳转,哪里还有什么效率和便捷。

具体一些:

  • 物以类聚 - 类似的操作放在一起,比如解析参数
  • 父子团聚 - 父方法在前,子方法紧挨着展开
  • 三八线 - 通过空行给眼睛一些提示,分割不同的“区块”

测试

我一直在逃避这一话题,对于开发者来说,自动化测试的收益不可限量。 我们随处都能见到测试的好处,数不胜数的工具和方法。 但是在实践中,我们总是花费很多的时间,却没有帮助我们解决问题。 似乎自动化测试是一种很低效的实践方式,他只是为了达成指标,为我们的工作带来负担。

事实上,高效的自动化测试是有一定的前提条件的:

  • 简洁、单一的实现
  • 待测逻辑要低耦合
  • 框架提供良好的支持
  • 持之以恒,日积月累

为什么我们的测试没有带来明显的收益?

  • 现有的系统本身缺少良好的基础,耦合度过高、缺少合适的抽象、太多的外部依赖等等,我们每次编写测试,都花费大量的时间来模拟依赖
  • 降低测试有效性的最常见原因就是重复,回想一下我们常见的数据查询场景,无数次重复的数据库链接、查询语句拼接、取值和资源释放,每次我们都要重复的编写这些重复的测试以覆盖各种边界场景,而我们实际关注的可能只有拼接出来的语句是否正确、是否在正确的地方取值。
  • 我们所使用的框架没有提供足够便利的工具集来帮助我们快速构建测试,如果能够通过依赖注入系统快速的构建依赖,或者通过模拟服务器提供模拟响应,我们编写测试的效率和有效性都会得到极大的提升。
  • 我们没有在短期内见到收益,当我们编写测试的功劳没有被发现,花费大量时间导致项目延期,或者失败的用例阻碍了我们的紧急工作时,我们常常会放弃他。

测试的作用在很多时候并不是直接产生收益的

除非拥有良好的框架支撑、或者测试能够直接帮助我们的工作时,我们才有驱动力来编写测试,甚至是以TDD方式来开始我们的编码工作。

例如:

  • 在开发后台插件的时候,框架本身提供的隔离和运行机制,让我们专注于独立的问题领域,并宽素完成测试用例的编写;同时,因为插件的运行和调试相对困难,需要配置和等待执行周期,相对来说通过测试来调试是多么幸福的体验。
  • 另外一面,在前端部分的开发体验中,我们编写的逻辑依赖于浏览器当前状态、html结构、css样式、并且要兼顾后台服务的响应和零散的全局变量,我们的关注点分散,而且处处受制,在没有框架支撑的情况下,编写测试的成本极高。在调试手段上,刷新一下浏览器是多么便捷。但是实际的代价就是,我们的验证过程是不可重复的,甚至这样的验证过程在积累到一定程度以后,会成为我们的负担。

即使不考虑直接收益,测试也能够在侧面为我们提供极大的帮助

  • 能测试的东西,一定是低耦合的
  • 能测试的东西,职责一定是单一的
  • 能运行的测试,本身就是说明文档
  • 完善的测试集,让我们放心的重构和扩展

    测试与质量是相辅相成,循环提升的关系。优秀的质量便于测试、丰富的测试帮助改善质量。虽然短期收益不明显,但是在反复循环中创造巨大的价值。

复利效应:复利效应指资产收益率以复利计息时,经过若干期后资产规模(本利和)将超过以单利计息时的情况。事实上,复利计息条件下资产规模随期数成指数增长,而单利计息时资产规模成线性增长,因此长期而言复利计息的总收益将大幅超过单利计息。

总的来说,我们始终在讨论时间和收益,最终的目标是获得良好的设计,帮助我们自己脱离困境

但是在当前情况下,我们最大的问题是没有时间,历史积累的债务拖慢了我们的脚步,阻止我们学习更好的技术、阻碍构建完整的测试集、迫使我们所在的团队无力改善组织结构和工作模式。

现实点说,我们能够做到的只有不要浪费时间、用尽可能少的成本带来收益,并且为以后的发展打好基础。 就像我们给出的方案,对于命名、长度、顺序的改进,节省了大家摸索和猜测的时间,成本很低,但是带来容易理解、单一职责、低耦合的设计。而且这样的实践是一次投入、持续收益,我们所做的仅仅是保持整洁

童子军规则:“让营地比你刚来时更干净。”

我们期望的结果是:

最后,请牢记,整洁是我们当前能够轻松做到的,但仅仅是开始,在释放出足够的时间后,我们需要继续走下去

  • 完善测试集,代替手工验证,让我们安心的进行修改
  • 引入各种模式和思想,来应对系统的发展和变化
  • 应用新的工具、技术、框架来改进我们的工作环境
  • 为团队提供信心和余力,改进组织架构和管理方式

自助者天助,助人着人助

延伸阅读

  • 《代码整洁之道》

  • 《重构:改善既有代码的设计》

  • 《敏捷软件开发:原则、模式与实践》