| name | how-to-solve-it |
| description | 基于 Polya《How to Solve It》的通用解题方法论。遇到复杂问题时使用。 |
How to Solve It(怎样解题)
源自 Polya 的启发式方法论,核心信念:解题是一种可以通过模仿和练习获得的实践技能。
所有问题——无论是实现新功能、修复 bug、还是重构——都遵循同一个基本流程。
最重要的原则:先猜后证。 完成的软件由测试构成,但创造中的软件由猜测构成。
四阶段模型
每个问题都经过四个阶段。跳过任一阶段都会导致返工或遗漏。
理解问题 → 制定方案 → 执行方案 → 回顾
(看清目标) (找到路径) (逐步验证) (从工作中提取最大价值)
各阶段不可跳过。 不理解问题就开始写代码是愚蠢的。不回顾就结束工作会浪费学习机会。
阶段一:理解问题
在开始任何修改之前,必须清晰地回答三个问题。
核心三问
- 未知量是什么? —— 到底要达成什么?是修复一个 bug?实现一个功能?理解一段逻辑?
- 已知数据是什么? —— 输入条件、约束、现有代码结构、相关 API、测试用例
- 条件是什么? —— 验收标准、边界条件、性能要求、兼容性约束
操作步骤
- 用自己的话复述问题。如果不能用简洁的语言说清楚问题,说明还没理解。
- 区分"求解题"与"求证题"(见下文问题类型)。不同类型的工具不同。
- 画图(如果适用)。架构图、数据流图、调用关系图。可视化暴露误解。
- 引入合适的记号。给关键变量、模块、状态命名。命名迫使你思考它们的含义。
- 问:条件足以确定未知量吗?条件多余?还是不足? —— 需求是否完备?是否有矛盾?
这个阶段的常见失败
- 没读完错误信息就开始猜原因
- 没理解现有代码的意图就开始改
- 只关注"怎么改"而不先弄清"为什么这样写"
阶段二:制定方案
从理解到方案的路径可能曲折漫长。方案的出现需要:已有知识的储备 + 良好的思维习惯 + 专注 + 一点运气。
寻找思路的核心策略
策略 1:找联系——寻找相关的问题
你有没有见过类似的问题?哪怕只有一点点相似?
在代码库中搜索类似模式、类似功能、类似 bug。看看别人是怎么解决的。这是最高效的起点。
看着未知量!想想有没有熟悉的、具有相同或相似未知量的问题。
如果你想实现一个缓存层,回忆你见过的缓存实现。如果要加一个 CLI 参数,看已有的参数是怎么加的。
策略 2:如果不能解决原问题,先解决一个相关的问题
你能不能想到一个更容易着手的相关问题?一个更普遍的问题?一个更特殊的问题?一个类比的问题?
- 更特殊的问题:先处理最简单的情况。写一个最小复现用例。hardcode 一个特例让它先跑通。
- 更普遍的问题:这个具体 bug 是不是某个更通用的边界条件问题的实例?
- 类比的问题:别的项目/模块怎么处理同类场景的?不同语言但同模式?
- 丢掉一部分条件:先忽略某个约束,把简化版做出来,再逐步加回约束。
策略 3:分解与重组——改变看问题的角度
你能不能换一种方式叙述问题?回到定义。从不同的角度去考虑。
- 把问题拆成子问题,逐个击破
- 从结果反推(逆向工作):如果期望的输出是 X,哪些中间状态能产出 X?
- 改变数据结构就改变了问题——有时候换一种数据表示方式,解法就显而易见了
策略 4:引入辅助元素
为了利用某个已知的模式或解法,是否需要引入新的辅助元素?
- 引入中间变量打破复杂表达式
- 引入抽象层/接口隔离两个紧耦合的模块
- 引入 mock/stub 隔离待测试单元
- 引入辅助函数提取公共逻辑
检查你的方案
在动手之前问自己:
所有数据都用上了吗?所有条件都考虑了吗?
如果方案中没有用到某个约束,要么那个约束是多余的,要么方案有漏洞。
渐进式提示原则
如果你卡住了,应该从最通用的问题开始,逐步具体化——不要一上来就给最具体的答案:
- "有没有相关的已知问题?"(最通用)
- "能不能利用类比?另一个模块怎么做的?"
- "要不要引入辅助元素?引入一个中间层?"
- 具体的实现提示(最后手段)
通用问题让你保持思考的主动权,具体答案剥夺了学习的机会。
阶段三:执行方案
执行比构想容易——主要需要的是耐心。
执行纪律
- 逐步验证:每一步都确认正确。直觉上"看起来对"和形式上"经过验证"是两回事。
- 区分大步和小步:复杂方案中区分主要步骤和次要细节。先验证大步骤,再处理细节。
- 不要忘记方案:如果方案是自己想出来的,你不会忘记它。如果是别人告诉你的,你很容易忘。
- 对每一步诚实:这一步你真的确信是对的吗?还是只是"大概没问题"?
具体到编程
- 每修改一个函数就跑相关测试
- 每完成一个子步骤就 commit(如果使用 git)
- 在改动的每个阶段都能编译通过(避免积累大量错误)
- 如果某一步卡住了,回到阶段二重新考虑方案,不要硬推
阶段四:回顾
这是最容易被跳过的阶段,也是最能提升能力的阶段。 即使优秀的程序员,完成工作后也常常直接切到下一个任务,错过了最重要的学习机会。
没有任何问题被完全穷尽。 总有余地改进解法,至少总能加深对解法的理解。
回顾检查清单
验证结果:
你能检验这个结果吗?你能检验论证过程吗?
- 测试是否通过?不只是 happy path,边界情况呢?
- 能否通过另一种方式验证?比如手工计算、不同的测试数据?
- 有没有更简洁直观的检验方式?能不能"一眼看出"结果是对的?
改进解法:
你能用不同方式推导出这个结果吗?能不能一眼看出?
- 解法是否足够简洁?有没有冗余步骤可以消除?
- 有没有更直观的实现方式?
- 能不能让代码更短、更清晰,同时保持正确性?
泛化与迁移:
你能在别的什么问题里利用这个结果或这个方法?
- 这个解法能泛化吗?别的地方有相同的模式需要修复吗?
- 这个方法(而不是结果)能用于其他场景吗?比如这次 debug 的思路?
- 有没有什么值得记录的经验?需要更新文档/注释吗?
回顾的实在收益
- 巩固知识:通过回顾,解法从"一次性答案"变成"可复用的工具"
- 发现新事实:回顾中经常发现更简洁的解法或意外的通用模式
- 培养直觉:反复回顾后,遇到类似问题时,好的做法会自然地跳入脑海
问题类型
求解题(Problems to Find)
定义:给定数据,求未知量。目标是产出一个结果。
软件工程等价:实现新功能、写一个新模块、设计一个算法、生成配置文件。
核心问题:未知量是什么?已知数据是什么?条件是什么?
关键工具:类比、分解、引入辅助元素、特殊化
求证题(Problems to Prove)
定义:给定假设,证明或否定一个结论。目标是判断真伪。
软件工程等价:调试 bug、验证某个怀疑、排查性能问题、审查代码正确性。
核心问题:假设是什么?结论是什么?
关键工具:逐步检查、反例(找一个让结论不成立的输入)、归谬(假设结论错误会导出矛盾)
在做之前先判断你面对的是哪种问题。 用错了工具会浪费大量时间。
启发式策略速查
以下策略来自 Polya 的启发式词典,适用于软件工程语境。
| 策略 | 含义 | 编程场景 |
|---|
| 类比 | 找一个类似的已解决问题,模仿其方法或利用其结果 | 参考同类模块的实现模式 |
| 辅助元素 | 引入新元素来搭建从已知到未知的桥梁 | 加中间层、辅助函数、wrapper |
| 分解与重组 | 拆成子问题,逐个击破后组合 | 分而治之、管道模式、模块拆分 |
| 回到定义 | 回到核心概念的定义,忘记惯常用法 | 重新理解 API 文档、语言规范 |
| 特殊化 | 先处理一个具体的特例 | hardcode 一个值让它跑通,再泛化 |
| 普遍化 | 从具体实例中发现通用模式 | bug fix 后发现同类 bug 的规律 |
| 逆向工作 | 从期望的结果反推需要的前提 | 从测试用例反推实现 |
| 变式 | 改变问题的某些方面看是否有新的思路 | 换一种数据结构、换一种算法 |
| 是否用上了所有数据 | 检查是否有未使用的约束或信息 | 某个入参/配置/边界条件是否被忽略了 |
| 检验维度 | 用量纲/单位检验合理性 | 检查类型一致性、边界值、空值处理 |
反模式:什么不该做
- 直接给答案。如果你在帮助别人(或自己在探索),不要跳过思考阶段给出具体答案。"试试用 Pythagoras 定理"不如问"有没有相关的已知问题?"
- 跳过理解阶段就开始写代码。这是最常见的错误。
- 跳过回顾阶段。工作"能跑就行"不是终点。
- 忽视类比。忽视已有代码库中的模式,重新发明轮子。
- 不区分求解题和求证题。用 debug 的方法去写 feature 或用写代码的方式去 debug,都会走弯路。
- 只用一种方式检验。直觉上觉得对 + 测试通过 > 只有直觉 > 只有测试 > 两者都没有。
核心习惯
反复练习直到它们成为本能:
- 面对任何问题时,首先问:未知量是什么?已知数据是什么?条件是什么?
- 卡住时,问:有没有相关的已知问题?能不能换一种方式叙述?
- 执行时,每一步都验证:这一步一定对吗?
- 完成后,回顾:结果能检验吗?能改进吗?能用在哪里?