本文的主要内容来自经典书籍<<程序员修炼之道>>, <<代码整洁之道>>和<<重构–改善既有代码的设计>>, 整理了这些书中我觉得比较重要的, 应该努力达到的要求.

提供各种选择而不要找蹩脚的借口

在接收一个任务后, 就相当于承诺了正确的完成这一任务. 因此需要对任务中可能出现的问题进行预估, 并主动承担相应的责任. 对于可能出现的风险应该提供相应的预案, 而不是在问题出现后声称”自己的代码被猫吃了”.

不要说事情做不到, 而是说明做什么可以挽回局面.

不要容忍破窗户

不要放过代码中存在的”破窗户”(例如低劣的设计, 糟糕的代码), 看到一个就修复一个, 并且要进一步的考虑该问题是否存在共性, 其他地方是否存在同样的错误. 实在无法修复的代码也应该进行一些处理, 防止这些代码扩散.

软件同样具有熵, 如果不经常性的对代码进行维护, 其熵值就会不断的增加, 并最终导致项目质量恶化.

定期为你的知识资产投资

知识和经验是最重要的职业资产. 然而这些资产是具有时效性的, 技术和市场需求的发展都会导致原有的一些知识过时. 个人所掌握的知识的价值降低会导致个人对于公司的价值降低. 因此, 为了避免这一情况, 需要考虑

  1. 进行定期投资. 不断地学习, 即使每次学习的内容不多, 也要保证不断地学习.
  2. 多元化. 知道的事情越多, 综合能力越强. 掌握工作中需要的知识只是底线, 需要主动的学习不同的技术, 不同的知识.
  3. 管理风险. 既要学习风险高回报高的技术, 也要学习风险低回报低的技术. 鸡蛋不放在一个篮子里.
  4. 低买高卖. 关注新技术, 新技术更有可能获得高回报
  5. 重新评估和平衡. 周期性的评估学到的知识的价值, 并根据情况作出调整.

基于以上内容, 可以指定一些较为具体的目标, 例如

  1. 每年学习一种新的语言
  2. 每季度阅读一本新的技术类书籍
  3. 阅读非技术类书籍
  4. 参加课程
  5. 加入本地用户组, 与其他人交流, 避免与世隔绝.

不要放过问题, 每个问题都是学习的机会

如果遇到了问题, 不要轻易的放过问题. 将找到问题的答案视为一个挑战, 通过查阅资料, 请教其他人等方式寻求答案. 与其他人交流的过程也是人际关系网络建立的过程. 即使无法解决现有问题, 在查阅资料和与其他人交流过程中附带的收获也能使自己的知识资产增加.

做好规划, 让自己在空闲时间总是有东西可以阅读.

交流

规划自己想说的内容, 写成大纲. 反复问自己”这是否讲清楚了我想说的全部内容?”, 直到确实如此为止.

要在脑海里形成听众的画面, 可以遵循WISDOM原则

  • What do you want them to learn
  • What is their interest in what you’ve got to say
  • How sophisticated are they?
  • How much detail do they want?
  • Whom do you want to own the information
  • How can you motivate them to listen to you?

即使是同一件事情, 对于不同的听众, 也应该采取不同的方法进行讲解. 例如有些听众希望简短, 而另一些听众希望详实. 不同领域的听众对于一个问题的关注点也往往不同. 例如市场部门关心产品的优势, 而技术部门关心技术价值.

不要重复自己&保持正交性

每次写代码都要问自己, 有没有产生不必要的重复? 有没有使得系统中的各个功能保持正交?

编写代码应该”羞怯”且”懒惰”, 对开始之前需要接受的东西要尽可能严格, 而允诺返回的东西要尽可能少. 如果一个函数可以接受任何东西并返回任何东西, 那就表明这个函数有大量的代码要写.

可撤销性

不存在最终决策, 需求总是会随着产品的开发发生变化, 任何当前看起来不会发生变化的假设在未来都可能发生变化, 因此要尽量保证代码架构的灵活性与可撤销性.

代码架构上的灵活可以使的模型的替换不影响其他部分, 可撤销性保证决策可以反悔.

估算

估算以避免发生意外. 在开发过程中有很多时候需要对问题进行估算. 估算的精度取决于问题的需要. 估算有两种思路, 第一种是将问题分解, 寻找其中的主要因素并估计主要因素的值. 只要分解过程正确, 则对主要因素的估计越准确, 则结果估计越准确. 第二种是向有类似经历的人请教具体花费的时间, 这往往能相当大程度的参考价值.

可以不断的追踪估计的结果, 分析估计值与实际值的偏差, 找出其中的原因并修复估算模型.

基本工具

知识的最佳存储方式是纯文本. 纯文本可以保证人类可读和机器可读, 因此相较于二进制文件通用性更强. 同时具有一定的自我解释性的纯文本也容易理解和处理. 在此基础上, 应该学习一种对于文本编辑更加合适的语言, 从而能够高效地处理文字.

Shell能通过组合的方式实现复杂的功能, 同时脚本可以将任何成功操作的序列记录并自动执行, 因此相较于GUI操作更能避免重复自己.

用好一种编辑器对于开发效率有很大的提高, 这种编辑器不一定非要是Vim或者Emacs, 只要能解决自己的需求, 就是合适的编辑器.

按照合约设计

在写每一个函数之前, 都应该考虑这个函数的前条件, 后条件类不变项是什么.

前条件是函数调用前必须为真的条件, 如果其不为真, 则函数一定不能调用. 后条件是调用函数后该函数保证一定为做的事情. 类不变项是函数调用之前和调用之后都应该为真的条件.

要崩溃不要破坏

程序发现问题时应该尽早崩溃, 而不是等错误产生更大的影响后导致程序崩溃.

怎样深思熟虑地编程

  • 总是意识到自己在做什么, 不要让事情慢慢失去控制
  • 不要盲目地编程, 不要构建自己不理解的应用, 或者使用不熟悉的技术, 不要被巧合误导
  • 按照计划行事
  • 依靠可靠的事情, 为自己的假定建立文档, 这有助于澄清头脑中的假定并有助于将其传达给其他人
  • 不要只测试自己的代码, 也要测试自己的假设. 使用断言判断自己的假设, 如果是对的, 则记录到文档之中, 否则应该庆幸提前发现了一个错误
  • 为工作划分优先级, 将时间用在最重要的事情上.
  • 不要被历史的代码限制, 项目中的任何代码都可以被重构.

重构

当代码具有如下的特征时, 应该考虑重构代码

  1. 重复
  2. 非正交的设计
  3. 过时的知识
  4. 性能不足

重构是一项需要慎重, 深思熟虑的活动, 需要注意以下几点

  1. 不要试图在重构的同时增加新功能
  2. 开始重构之前, 确保拥有良好的测试, 并尽可能经常性的运行这些测试. 这样可以保证如果发生破坏能够尽快知道.
  3. 采用短小, 深思熟虑的步骤. 每个步骤保持短小, 并进行测试能够避免出现问题后的长时间调试

无情的测试

大多数开发者都讨厌测试, 往往会下意思的避开代码的脆弱之处. 但与此相反, 测试就应该是发现代码的脆弱之处的. 要早测试, 常测试, 自动测试.

测试代码是否正确也需要测试, 可以通过”蓄意破坏”代码来测试现有的测试代码能否正确的捕捉错误.

测试应该做到一个BUG只抓一次. 发现了一个BUG后就应该加入到测试用例之中, 避免同样的错误再次出现.

变量命名

变量命名是代码整洁的基础, 应该努力保证

名副其实, 变量的名字能够准确的表达变量的含义. 如果发现不名副其实的变量名, 就应该立刻修改. 变量名应该尽量保持准确, 而不要使用过于抽象的名字, 例如theList

做有意义的区分. 不要定义太多类似的变量, 例如Product和ProductData, ProductInfo之间就看不出太多的差异. 一些前缀, 比如a, the, 以及一些后缀, 比如Table, Variable都是没有意义的单词, 这种信息对于理解变量的含义没有任何的意义.

编写函数

编写函数最重要的思想是保持短小. 一个短小的函数总是比一个长函数更容易理解.

每个函数只做一件事. 如果一个函数不能简单的概括要做什么, 就说明抽象程度不够, 应该进行拆分. 每个函数位于一个抽象层次. 阅读函数的过程中, 应该能有一种为了达到A目的, 首先设置B, 然后执行C; 为了设置B, 首先执行D, 然后执行E的逻辑顺序.

函数的参数应该越少越好, 最好是没有参数, 其次是1个参数, 再次是2个参数. 3个及以上参数的函数都不便于理解, 出现这种情况时应该对参数封装为合适的结构体. 函数的参数不应该有布尔变量, 这种情况必然违背了一个函数只做一件事的原则.

分割指令和询问. 一个函数要么完成一件事, 要么回答一个询问. 不要让一个函数即完成指令又回答询问, 这依然违背了一个函数只做一件事原则.

写代码就像写论文, 很难一次性写出满足规则的代码, 所以通常都是写写出需要的代码, 然后逐步调整成需要的样子.

代码注释

最好的注释就是不写注释. 如果通过合适的函数命名, 使得代码具有很高的可读性, 则不需要任何注释. 如果注释用于解释代码, 那么就应该考虑是不是代码写的不够清晰.

代码的坏味道

以下是一些常见的代码的坏味道, 出现这些情况的时候, 就应该考虑进行一些重构.

  • 无法理解的变量名, 与实际含义不匹配的变量名
  • 重复的代码
  • 过长的函数. 函数应该保持短小, 通过合适的函数命名使得代码中不需要再进行额外的注释
  • 过长的参数列表
  • 全局数据和可变数据. 变量作用域越大, 就越应该限制其可变性. 可以计算出来的数据就不应该直接存储.
  • 发散式变化和霰弹式修改. 如果一个修改需要同时修改代码的多个地方, 则说明代码设计存在问题.
  • 数据泥团: 如果多条数据总是一起出现, 则应该把这些数据放在一起, 并据此分析是否应该增加数据封装.
  • 基本类型偏执. 根据实际问题的需要创建一些数据类型, 避免始终使用基本数据类型. 例如电话号码就应该是一种类型而不是一个字符串.
  • 夸夸其谈通用性: 如果一个抽象设计根本就用不上, 那么它就是多余的.
  • 临时字段: 类中的某些字段仅在某些情况中使用, 而在另外一些情况中不使用.
  • 中间人: 某个类的大部分操作是将当前请求转发给另一个类
  • 内幕交易: 两个模块通过隐含的模式交换数据, 常见于子类隐含的访问父类的数据
  • 被拒绝的遗赠: 子类未实现父类的所有行为或不需要父类的某些字段或方法.
  • 注释: 写注释前想一想是否可通过合理的代码结构消除注释的必要.

构筑测试体系

在重构之前一定要确保系统中具有较为充分的测试用例, 并且能够快速, 频繁的执行测试用例. 编写测试用例时应该注意如下的一些点

  • 确保测试不通过时确实会失败: 可在代码中人为产生一些错误, 使得每个测试用例都确实的失败过.
  • 频繁的运行测试用例: 及时通过测试用例发现问题, 避免进行大量修改后才发现问题.
  • 关注重点逻辑: 编写测试用例也具有优先级, 优先关注重点的逻辑
  • 不要重复出现BUG: 排查出任何BUG后, 都需要将其加入测试用例, 以避免重复出现同样的BUG

基础重构

  • 提取函数: 如果一段代码需要用注释来解释, 那么就应该考虑将其抽取为一个函数, 用函数名说明执行逻辑
  • 内联函数: 如果一个函数的内容就是调用另一个函数, 则应该考虑这个层次的抽象是否必要
  • 提取变量: 对于一个复杂的表达式, 可以考虑使用具有意义的变量表示表达式中的一部分, 从而提高可读性
  • 内联变量: 如果一个变量并不能提供额外信息, 则可以考虑直接将变量替换为表达式
  • 引入参数对象: 如果某一组参数总是同时出现, 则应该考虑将他们打包为一个参数对象, 并考虑这种组合是否对应某种抽象
  • 函数聚合为类: 如果一组函数总是紧密的出现, 则应该考虑将这些方法和对应的数据打包为类
  • 拆分阶段: 如果一个函数中在交替的做两件不同的事情, 则应该考虑将两件事情分隔开, 分别封装为函数

封装重构

  • 封装记录: 许多语言会提供类似Python的Dict的可变数据结构, 在代码中跨越大量函数的使用这类可变结构可能导致代码难以理解. 在合适的时候应该将这些数据封装为合适的类, 使用类传递数据
  • 以对象取代基本类型. 许多数据并不能使用基础类型搞定, 例如电话号码从存储上来看就是一个字符串, 但实际使用中可能需要格式化函数等辅助函数, 应该将这些封装为一个类
  • 提炼类: 如果某些数据或者某些方法总是一起出现, 则应该考虑将其封装为一个类. 在开发过程中, 可以渐进的考虑这个问题, 逐步的优化数据抽象.
  • 内联类: 与提炼类相反, 如果某个类的功能已经萎缩到几乎没有, 则可以考虑将其内联到外部的类之中
  • 隐藏委托关系: 使用封装时应该尽量隐藏内部的实现细节, 使得外部调用时不依赖内部实现. 因此一个链式的调用通常认为是暴露了内部细节, 应该考虑对这类调用进行封装.
  • 取消中间人: 与此相对地, 过度的封装可能导致大量的转发代码, 增加无效的工作量, 此时应该考虑去除中间转发代码, 直接操作底层的对象.

封装是一个渐进的过程, 几个月之前的封装在几个月之后可能就是过度的, 因此需要根据代码的变换不断地修改封装的程度.

重新组织数据

  • 拆分变量: 一个变量只表达一个含义, 不要把多件事情使用一个变量表示
  • 以查询代替派生变量: 如果一个数据可以简单的计算出来, 则应该直接计算而不是使用变量存储

简化条件逻辑

  • 分解条件表达式: 原则上, 如果一个函数包含一组if-elseif代码块, 则其中的每一块逻辑都应该是一个函数, 以便于降低代码阅读难度
  • 合并条件表达式: 如果多个条件分支对应的行为相同, 则应该将这些条件合并为一个分支
  • 使用卫语句替换嵌套的条件表达式
  • 以多态取代条件表达式

搬移特性类

  • 移动函数/字段: 函数应该和它的上下文紧密存放在一起, 如果一个函数经常引用另外一组数据, 则应该将函数移动到对应的类之中.
  • 拆分循环: 一次循环只做一件事 & 使用管道模式替换直接的循环

重构API

  • 将查询函数和修改函数分离: 一个函数只做一件事
  • 函数参数化: 让函数接受一些参数, 使得代码可以在几个类似的场景中复用
  • 移除标记参数: 标记参数就是在表明一个函数可根据标记做不同的事情, 应该拆分为多个函数
  • 保持对象完整: 如果经常将一个对象中的几个字段取出来传递给其他函数, 则应该考虑直接传递这个对象, 或者对象字段划分是否合理
  • 以查询取代参数: 如果某个参数可以在函数内部通过查询获得, 可考虑减少参数的数量, 从而降低调用放的使用难度
  • 以参数取代查询:如果函数内部的查询产生了某些不合适的依赖关系, 则可以将查询变为参数, 由调用方解决依赖关系

最后更新: 2024年04月18日 13:26

版权声明:本文为原创文章,转载请注明出处

原始链接: https://lizec.top/2022/07/06/%E7%A8%8B%E5%BA%8F%E5%91%98%E4%BF%AE%E7%82%BC%E4%B9%8B%E9%81%93%E9%98%85%E8%AF%BB%E7%AC%94%E8%AE%B0/