本文的主要内容来自经典书籍<<程序员修炼之道>>, <<代码整洁之道>>, <<重构–改善既有代码的设计>>和<<高效程序员的45个习惯>>, 整理了这些书中我觉得比较重要的, 应该努力达到的要求. 并且结合我自己的开发经验, 总结了一些原则性的知识.

架构设计原则

Item 1 不要重复自己

重复几乎可以说是万恶之源, 许多编程方式和设计模式本质上就是为了避免重复. 每次写代码都要问自己, 有没有产生不必要的重复?

Item 2 保持正交性

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

无关的事情之间不应该相互影响. 保持正交性可以使得修改局部化, 降低开发的风险. 保持正交性还可以提高代码的复用性, 通过组合实现新的功能.

Item 3 可撤销性

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

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

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

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

Item 4 先使用再实现

先考虑如何使用一个函数, 先编写使用和测试的代码, 从而站在代码用户的角度来思考, 而不仅仅是一个单纯的实现者. 从使用的角度考虑, 可以设计一个更有用, 更一致的接口.

Item 5 用约束代替规范

如果一个规范问题可通过程序进行约束, 则优先考虑程序约束. 规范本质上依赖于人准守, 但实际情况中可能存在规范宣讲不到位, 实际执行不准确约束等问题. 或者说, 只要是依赖于人为准数的准则, 就一定存在违反的情况. 只有通过自动化的约束定义的准则, 才能保证不会违反.

因此在提出规范性问题的同时, 就要考虑能否通过程序进行约束, 实现一定的强制效果.

方案设计原则

Item 1 对齐开发流程

多人协作开发时, 可能存在开发流程理解不一致的问题, 或者都认为某个功能应该对方开发. 开发之前就要明确开发的主要流程, 确认整个链路执行过程达成一致.

Item 2 一般化解决问题

方案设计时, 除了要完成给定的目标以外, 还需要从系统层次考虑问题. 例如

  • 是否存在方案能够在解决当前问题的同时, 可一并解决其他问题, 从而达到更好的效果.
  • 是否存在方案能够一般化的解决此类问题

Item 3 考虑异常情况

对于大部分代码功能而言, 其存在操作成功或者操作失败两种情况. 对于业务而言, 可能并不太考虑操作失败的情况(例如概率低, 或者业务无法处理), 但从程序设计的角度, 应该需要充分考虑此类情况.

  1. 是否会产生操作失败的可能性?
  2. 业务层面如何得知操作失败?
  3. 操作失败后业务可以做什么?

例如对于数据库操作, 可通过返回影响行数判定操作是否符合预期. 业务方在不符合预期的时候可采取其他方案进行处理.

Item 4 预先评估影响范围与修改量

在方案设计时, 需要充分地评估影响范围和修改量. 而不要等到实际编码阶段再去进行评估, 否则可能因为前期评估不足导致在临近上线前需要进行大幅度的修改.

Item 5 抛出问题并提供解决方案

对于技术方面的问题, 在抛出问题的同时, 还需要提供解决方案. 避免在毫无解决方案的情况下提出问题, 技术问题需要技术解决, 最终还是要提出解决方案的.

编码原则

Item 1 使用名副其实的变量名

变量命名是代码整洁的基础, 应该做到名副其实而不存在误导. 变量的名字能够准确的表达变量的含义, 不要使用完全抽象的名字(例如theList). 不要使用俚语. 如果发现不名副其实的变量名, 就应该立刻修改.

变量的作用域越大, 则携带的信息就应该越多. 例如全局变量使用具有说明性的名字, 而局部变量使用较短的名字. 人们常常鼓励程序员始终使用长的变量名. 这种认识完全是错误的, 清晰性经常是随着简洁而来的.

变量之间应该做有意义的区分, 如果两个单词含义基本接近, 就无法看出差异. 例如ProductProductData, ProductInfo之间就看不出太多的差异.

一些前缀(a, the)以及一些后缀(Table, Variable)都是没有意义的单词, 不要作为变量名的一部分.

使用可以读出来的变量名, 否则对于代码交流会产生困难. 阅读代码的人一定是程序员, 因此放心使用计算机领域的专有名词.

Item 2 一个函数只做好一件事

编写函数最重要的思想是每个函数只做一件事. 一个函数要么完成一件事, 要么回答一个询问. 不要让一个函数即完成指令又回答询问. 如果一个函数不能简单的概括要做什么(例如无法取一个合适的函数名), 就说明函数承担了多种功能, 需要进行拆分.

为了保证函数只做好一件事, 需要对函数的内容进行合适的抽象, 使得不同抽象层次的内容分布在不同的函数中, 而每个函数仅处理自己所在层次的内容. 阅读函数的过程中, 应该能有一种为了达到A目的, 首先设置B, 然后执行C; 为了设置B, 首先执行D, 然后执行E的逻辑顺序.

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

常见的函数坏味道包括: 过多的参数, 输出参数, 表示参数, 未调用的函数

Item 3 正确的使用注释

最好的注释就是不写注释. 如果通过合适的函数命名, 使得代码具有很高的可读性, 则不需要任何注释.

注释应该用于解释设计意图, 阐释特定取值的含义, 提供警示和放大某些特殊之处. 其余的注释都应该通过代码本身的命名实现自解释.

常见的注释坏味道包括: 可用版本管理系统维护的信息, 废弃的注释, 完全冗余的注释, 注释掉的代码

Item 4 使用一致的编码风格

一致性带来的将是更好的程序. 如果相同计算的每次出现总是采用同样方式, 任何变化就预示着是经过了深思熟虑, 要求读程序的人注意.

如果你工作在一个不是自己写的程序上, 请注意保留程序原有的风格. 当你需要做修改时, 不要使用你自己的风格, 即使你特别喜欢它. 程序的一致性比你本人的习惯更重要, 因为这将使随你之后的其他人生活得更容易些.

Item 5 不要放过奇怪的事情

如果在程序运行过程中发现了奇怪的事情, 一定要充分的排查清楚. 如果感到奇怪, 则必定包含当前无法理解的行为, 这很有可能就是隐藏的BUG.

Item 6 设置合理的兜底条件

对于存在正反两种情况的函数, 需要合理的考虑选择哪一种情况作为兜底条件.

例如获取分布式锁的函数, 默认应该将获取失败作为兜底条件. 相较于全部都获得锁导致重复执行, 全部无法获得锁将会导致立即发现问题, 从而更快的解决问题.

如果抛出了一个错误, 一定要向上确认链路中对错误进行了处理. 如果上游链路直接无视错误继续执行, 则需要进一步评估该错误的影响. 如果无法确认影响, 尽力交付相比抛出错误可能是更好的选择.

例如在反序列化的过程中, 如果无法确认反序列化的类型, 则有两种选择:

  1. 尝试基础的反序列化方法, 并报错错误
  2. 不进行任何反序列化操作, 直接报告错误

如果上游业务直接无视错误继续执行, 此时尝试用基础方法兜底的兼容性要优于不做任何事情.

Item 7 通过标记隔离环境

如果一个组件涉及环境隔离问题, 例如在预发环境产生的数据不可被生产环境处理, 或者测试环境之间需要隔离, 则考虑引入环境变量标记

  • 在产生数据时附带环境变量标记, 从而不同环境产生的数据天然具有不同标记
  • 在处理数据时检查环境标记, 各环境仅处理自己环境的数据

基于上述方案, 可以简单地实现隔离效果.

Item 8 不要容忍破窗户

不要放过代码中存在的”破窗户”(例如低劣的设计, 糟糕的代码), 看到一个就修复一个, 实在无法修复的代码也应该进行一些处理, 防止这些代码扩散.

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

Item 9 解决共性问题

在编码过程中发现一个问题, 就应该进一步的考虑该问题是否存在共性, 其他地方是否存在同样的错误.

Item 10 要崩溃不要破坏

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

Item 11 区分技术错误与业务错误

对于一个业务系统, 实际存在两种完全不同的错误类型, 一种是技术错误(例如RPC失败, 数据库异常), 一种是业务错误(例如用户转账但余额不足). 对于绝大部分编程语言, 都只有一种错误返回机制, 而不会显示的区分这两类错误.

但对于业务系统而言, 必须明确区分并进行不同的处理. 技术错误通常能采取的措施非常有限, 因此一般需要将错误抛到最顶层, 由框架进行妥善的处理. 而业务错误则需要对应的业务逻辑捕获错误并进行处理.

Item 12 前端校验和后端校验

前端校验的目的是防止用户错误输入, 后端校验的目的是防止恶意攻击. 所以一些关键的校验既需要前端校验也需要后端校验.

Item 13 让接口易于使用而难于错误

从用户视角来开发接口, 并且持续进行改进. 主动分析并观察收集用户如何错误使用接口, 改进接口避免这些问题. 接口的价值永远服务于使用者而非实现者.

Item 14 谨慎修改协议中的字段类型

对于绝大部分语言, 协议的字段类型是一种基础假设, 很大程度上会影响对控制的判定等各种细节问题. 贸然修改字段类型容易导致其他业务逻辑出现未预料到的情况, 导致进入错误的逻辑分支.

例如在GO语言中, string类型对于空值仅需要判断len, 但对于interface{}, 则需要更复杂的处理. 如果贸然将string类型替换为interface{}类型, 并直接使用SPrintf进行转换, 则当参数为nil时会产生字符串"<nil>", 导致判断逻辑出现非预期的表现.

Unix设计哲学

Item 1 小就是美

当你开始写一个程序, 从小规模开始并尽量保持. 需要抵制让程序变为庞然大物的诱惑. 或者更直接地, 让每一个程序只做好一件事

Item 2 尽快建立原型

Item 3 舍弃高效率而取可移植性

硬件在快速发展, 针对特定硬件的高效率实现可能随着硬件的废弃一同失效. 具有可移植性的软件反而可以随着硬件性能的提升而获得加速. 因此对于不同硬件, 不同平台的兼容性/可移植性比效率更重要.

同理, 使用使用纯文本文件来存储数据保证了在不同软件, 不同系统中可以通用, 同时也保证了人类可读.

Item 4 充分利用软件的杠杆效应

代码开发出来就应该能够复用. 既要保证自己写出来的代码可以被其他人复用, 也要多复用其他人的代码.

使用shell脚本来提高杠杆效应和可移植性

Item 5 避免强制性的用户界面

让每一个程序都成为过滤器. Unix经典的一种模式是使用管道串联不同的指令, 从而通过组合实现复杂功能.

Item 6 寻求90%的解决方案

测试原则

Item 1 无情的测试

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

Item 2 测试代码的正确性

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

Item 3 不要重复同样的错误

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

Item 4 测试自己的假设

不要只测试自己的代码, 也要测试自己的假设. 使用断言判断自己的假设, 如果是对的, 则记录到文档之中, 否则应该庆幸提前发现了一个错误

Item 5 FIRST原则

快速(Fast): 测试用例应该可以快速执行, 较长的测试用例执行时间将导致不愿意频繁的运行测试用例

独立(Independent): 每一个测试用例都应该是相互独立的, 不应该相互影响

可重复(Repeatable): 测试用例应该可以在任何环境可重复, 最好能直接在本地执行

自足验证(Self-Validating): 测试用例应该输出布尔结果, 而不应该通过查看日志判断是否正确

及时(Timely): 测试用例应该及时编写, 先于代码之前编写

重构原则

Item 1 确定重构的时机

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

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

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

  1. 不要试图在重构的同时增加新功能. 不要视图重写一切, 要尽量复用已有的功能, 避免重复修复已知问题.
  2. 开始重构之前, 确保拥有良好的测试, 并尽可能经常性的运行这些测试. 这样可以保证如果发生破坏能够尽快知道.
  3. 采用短小, 深思熟虑的步骤. 每个步骤保持短小, 并进行测试能够避免出现问题后的长时间调试
  4. 代码风格或结构不符合个人偏好不能成为重构的正当理由. 认为自己比前序开发者更高明同样不是有效依据

Item 2 代码的坏味道

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

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

Item 3 构筑测试体系

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

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

Item 4 基础重构

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

Item 5 封装重构

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

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

Item 6 重新组织数据

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

Item 7 简化条件逻辑

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

Item 8 搬移特性类

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

Item 9 重构API

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

项目开发原则

Item 1 平衡demo验证与项目开发

在验证新技术或者新方案的可行性时, 通常会考虑实现一个demo项目. 此时主要考虑的是快速验证可行性, 对于架构和实现方式的要求不高. 在开发过程中也不会追求封装, 更多偏向于暴力面向过程.

但需要注意, 如果后续将demo转换为实际使用的项目时, 必须重新进行架构设计, 先设计好整体结构, 再进行后续开发. 否则由于前期追求快速实现, 可能导致封装性不足或者架构设计不合理, 导致后期需要花费同样多甚至更多的时间进行重构.

如果一个项目在重构的时候都不已经需要思考怎么重构就可以进行重构, 就说明一开始架构就没有设计好, 留下了明显的问题. 写代码的能力可以在开发中反复锻炼, 但项目设计的能力并不能, 因此要主动的进行项目设计.

Item 2 命令行还是GUI

命令行操作具有更好的相互调用性, 可以容易的和其他命令组合实现自动化操作和复杂的步骤操作. 使用GUI具有更好的人工交互性, 方便用户进行操作.

虽然对于程序员而言, 使用命令行并不会有多麻烦, 但历史的发展已经证明了, 通过鼠标点一点就能完成的任务, 没有人愿意使用命令行输入.

尤其在命令行输入较为复杂的情况下(例如需要输入复杂的路径信息, 或者复杂的参数), 使用鼠标操作明显由于命令行操作.

对于一个程序, 同时支持GUI操作和命令行操作可能是一个比较合适的选择. 例如Vscode的指令即可以在GUI中操作, 相同的指令也可以用命令行调用

Item 3 使用标准解决方案

在开发过程中的许多问题, 在业内已经具有了标准的解决方案. 对于这类问题, 应该优先使用标准解决方案, 而不要随便使用自定义方案.

例如对于编程语言的分析, 最常规最可维护的方式就是准寻 词法分析 -> 语法分析 -> 语义分析 的步骤进行处理. 只要待分析的语言具有足够的复杂性, 则整个开发流程最终就必然会回到这一路径之上, 否则各层次之间相互耦合将导致程序复杂性过高而无法维护.

再例如前后端数据传递, 常规做法是传递JSON, 无论是前端语言还是后端语言对于这一操作都具有良好的支持. 而如果非要把数据组装成JS代码, 然后在前端执行JS代码就非常的蹩脚, 而且还会引入安全性的问题.

接口切换原则

Item 1 考虑对账与回滚策略

对于任何流量切换类操作, 需要考虑如下两个问题

  1. 如何进行对账. 只要具有对账条件, 一定要进行对账. 减少操作失误产生的风险.
  2. 如何进行步骤划分. 能拆分执行就拆分执行. 每一步是否可以回滚. 不能回滚的步骤需要重点评估.

Item 2 考虑稳妥的代码切换

  1. 不要直接信任任何接口. 接口有可能声明了具备某种能力, 但实际并未正确实现. 一定要直接验证是否可行, 而不能盲目信任接口.
    1. 可以阅读代码, 确定实现逻辑 / 找对应的开发人员确认有无特殊处理
  2. 不要直接新增没有测试过的代码逻辑. 也许代码的实现与理解的代码实现并不一致, 在没有测试的情况下可能出现错误使用的情况.
  3. 不要在无法观察的情况下上线代码. 一定要想办法观察到代码的效果, 如果无法观察到则无法及时发现问题.

Item 3 考虑调用频率与保护

在调用一个接口时, 应该向下游业务方进行一些基本信息的确认, 包括

  1. 接口的性能情况, 预期可以使用多大的频率进行调用
  2. 接口是否具有限频与熔断, 如果有, 阈值是多少
  3. 业务方对于调用该接口是否还有其他补充

尤其注意在调用不熟悉的接口时, 应该主动向业务方询问是否有其他注意事项, 以免由于缺乏信息产生错误的判断

学习原则

Item 1 定期为你的知识资产投资

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

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

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

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

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

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

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

Item 2 迭代和增量式的学习

每天计划用一段时间来学习新技术,它不需要很长时间,但需要经常进行。记下那些你想学习的东西——当你听到一些不熟悉的术语或者短语时,简要地把它记录下来。然后在计划的时间中深入研究它。

你要明白为什么需要这项新技术——它试图解决什么样的问题?它可以被用在什么地方?

避免在一时冲动的情况下,只是因为想学习而将应用切换到新的技术、框架或开发语言。在做决策之前,你必须评估新技术的优势。开发一个小的原型系统,是对付技术狂热者的一剂良药。

在学习一门新技术的时候,多问问自己,是否把太多旧的态度和方法用在了新技术上。学习面向对象编程和学习面向过程编程是截然不同的。

Item 3 打破砂锅问到底

不停地问为什么。不能只满足于别人告诉你的表面现象。要不停地提问直到你明白问题的根源。你可能会跑题,问了一些与主题无关的问题。就好比是,如果汽车启动不了,你问是不是轮胎出了问题,这是没有任何帮助的。问“为什么”,但是要问到点子上。

当你问“为什么”的时候,也许你会被反问:“为什么你问这个问题?”在提问之前,想好你提问的理由,这会有助于你问出恰当的问题。“这个,我不知道”是一个好的起点,应该由此进行更进一步的调查,而不应在此戛然结束。

Item 4 防微杜渐

在工作压力之下,不去深入了解真正的问题以及可能的后果,就快速修改代码,这样只是解决表面问题,最终会引发大问题。

孤立非常危险,不要让开发人员完全孤立地编写代码。如果团队成员花些时间阅读其他同事写的代码,他们就能确保代码是可读和可理解的,并且不会随意加入这些“+1或-1”的代码。你必须要理解一块代码是如何工作的,但是不一定需要成为一位专家。

负载与流量估计

Item 1 评估容量

当引入存储组件(Redis, DB等)时, 需要评估存储的容量, 可以考虑如下的几个方面

  1. 历史上会产生多少数据
  2. 会新增多少数据
  3. 是否会产生热点数据

Item 2 评估数据库负载

对于云数据库, 可根据标明的IOPS和最大链接数评估可承载的业务量.

在任何情况下, 只要能使用连接池, 则都应该使用连接池访问数据库. 频繁的创建和销毁连接会产生非常大的开销. 在极限情况下, 创建和销毁连接消耗的资源可能比查询和插入数据的消耗还大.

配置连接池时, 除了指定最大空闲连接数和最大连接数以外, 还需要指定最大生存时间. 对于数据库的连接, 其可用时间并不是无限大的. 通过设置最大生存时间, 可避免数据库端关闭连接后, 客户端还使用该连接, 导致请求错误.

交流

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

要在脑海里形成听众的画面, 可以遵循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, 只要能解决自己的需求, 就是合适的编辑器.

按照合约设计

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

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

怎样深思熟虑地编程

  • 总是意识到自己在做什么, 不要让事情慢慢失去控制
  • 不要盲目地编程, 不要构建自己不理解的应用, 或者使用不熟悉的技术, 不要被巧合误导
  • 按照计划行事
  • 依靠可靠的事情, 为自己的假定建立文档, 这有助于澄清头脑中的假定并有助于将其传达给其他人
  • 为工作划分优先级, 将时间用在最重要的事情上.
  • 不要被历史的代码限制, 项目中的任何代码都可以被重构.

最后更新: 2026年04月22日 21:55

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

原始链接: 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/