这次遇到的事情,表面上像是“把一个软件研究明白,再照着做出来”。但真正开始以后,我很快发现这不是一个动作,而是两类完全不同的工作:一类要回答原系统到底做了什么,另一类要把这些认识变成可以运行、可以比较的结果。
当时我们讨论两个方向怎么配合。一个方向更擅长深层静态分析、追调用关系和整理证据;另一个方向更擅长调查、编码、接口回放、版本对比和继续修改。最初看起来,把它们接成一条流水线就够了:前面分析,后面实现。后来我意识到,关键不在于前后顺序,而在于中间必须有一份能核对的规格。
软件拆解与软件重建是两种不同的判断活动,最好不要让同一条线同时承担“猜事实”和“写实现”。
看起来只是复制,实际上要先解释
用户后来问,重建工具是不是可以理解成“复制一切”。这个说法有一半接近,但也容易把问题带偏。重建不是把看到的文件、界面或几个成功案例抄一遍,而是尽量复现原系统对输入的反应、对外提供的接口、关键状态变化,以及用户能观察到的结果。
拆解则要反过来问:哪些行为是稳定的,哪些只是某个版本的偶然现象?一个函数名出现了,不代表它一定会被当前动作调用;一次接口返回了结果,也不代表所有请求都遵循同一条规则。只有把证据、推断和未知分开,后续实现才不会把猜测伪装成需求。
如果不先处理这层区别,最容易出现的后果是:实现线把一次观测当成完整规格,做出一个“看起来像”的版本;分析线又因为已经有人开始写代码,不断为现有实现找解释。最后两边都很忙,却没有人能说清楚哪些行为已经证实,哪些只是暂时假设。
一条线负责证据,一条线负责实现
后来形成的判断很明确:分析线的交付物不应该是一堆零散结论,而应该是功能规格和重建义务台账。规格描述“输入是什么、系统做什么、输出是什么、边界在哪里”;义务台账则记录“重建时必须实现什么、证据来自哪里、当前还缺什么验证”。
这份台账的价值,在于它把两种语言接起来。分析线可以写“某个请求在特定游标下返回两页内容,响应中有一个每次都会变化的请求标识”;建造线就知道,不能把那个标识写死,也不能因为它变化就误判整个响应不一致。分析线还可以写“某些函数只是位置变化,不应算作逻辑变化”;建造线就知道,版本对比不能只看地址或原始字节。
建造线拿到的也不只是一个“请复刻”的目标,而是一组可以逐项完成的义务:先搭出能运行的最小闭环,再补接口边界,再处理版本差异,最后把仍然不一致的地方列回缺口。这样做的好处不是分工好看,而是每一次修改都能回答“它是在满足哪条规格”。
分析线只产出证据、规格和未决问题,建造线只按规格实现并留下可回放的结果。
清洁边界比并行更重要
分成两条线,并不等于两边可以随意共享半成品。真正需要建立的是清洁边界:分析线交付证据包、行为描述和重建义务;建造线在隔离条件下实现,不把未公开的内部猜测直接带进代码;验收线再用统一输入去比较两边的结果。
这一步听起来有点保守,但它能防止一种很隐蔽的污染。假设分析者已经看到了某个内部实现细节,建造者又根据这个细节写出代码,那么最后测试通过,并不能说明规格正确,只能说明实现复用了原来的答案。更稳的做法是把能影响实现的内容翻译成行为约束,把不必要的内部细节留在分析侧。
这不是为了制造形式上的隔离,而是为了让重建结果具有独立性。即使换一个人、换一个实现方式,只要遵守同一份规格,也应该能得到相同的外部行为。一个流程成熟的标志,不是只有原分析者才能把它跑通,而是换人以后仍然知道该验证什么。
验收不能只看功能通过
重建完成后,至少要做两种验收。第一种是行为覆盖:用明确的输入场景回放原系统和重建系统,比较接口响应、状态变化和错误处理。第二种是视觉比对:对同一操作截取结果,比较布局、层级、间距、文字和状态反馈。
只做功能覆盖,会漏掉很多用户真正能感知的问题。按钮能点击,不代表位置对;请求返回相同字段,不代表页面在加载、空状态和错误状态下表现一致。反过来,只做视觉比对也不够,因为一张截图无法证明隐藏状态、版本差异和边界输入都被处理了。
这次工作的验证经验也说明,比较工具本身要足够谨慎。请求中有每次变化的标识时,不能把它当成业务差异;响应里有超出常规精度范围的数字时,不能先把数据转成会丢精度的类型;函数只是搬了位置时,也不能直接判定实现改变。验收规则必须先区分“变化是噪声、布局变化,还是行为变化”。
功能覆盖回答“能不能做”,视觉比对回答“做出来是不是同一个产品体验”,两者缺一不可。
缺口要回到分析线补查
两条线的闭环,不是分析完成后就永远不再回头,而是让回头变得有边界。建造线每次对比后,都应该把不一致归类:是规格遗漏、实现错误、测试输入不完整,还是原系统本身存在版本差异。只有前两类和明确未知,才值得触发增量补查。
这里最容易误判的,是看到差异就继续扩大分析范围。这样做会让拆解不断膨胀,建造也没有稳定的收敛点。我更愿意采用“针对缺口补查”的规则:先写清这次差异具体表现在哪里,再问补哪条证据可以消除它,最后只把新增规格交给建造线继续实现。
比如,接口回放已经证明请求路径和返回内容一致,但视觉结果仍有差异,就不必重新分析整个系统,而是检查资源加载、字体、默认状态或时间相关字段。又比如,版本对比发现函数体哈希变化,也不能立刻推断用户行为变化,应该先确认这个函数是否落在当前动作的调用链上。
这种闭环让我重新意识到,所谓“继续努力”不应该等于无止境地扩大任务。真正有效的继续,是让每一次返工都对应一个可描述的缺口。这样分析与重建才会逐渐收敛,而不是互相制造新的不确定性。
当前阶段不必立刻做满复刻
还有一个实际取舍:如果当前工作主要目标仍然是分析,那么离线回放验证和版本对比的直接收益,可能高于马上投入完整的复刻闭环。先把已经知道的接口和行为固定下来,可以帮助我们判断哪些部分值得重建;如果还没有稳定规格,过早搭建完整产品,只会把不确定性包装成更多代码。
我选择把完整复刻放在规格逐步稳定之后,是因为它能避免两种浪费。第一种是重复实现后来被证明不成立的假设;第二种是为了配合早期代码,不断修改分析结论。对当前阶段来说,先完成离线回放、版本差异和证据整理,已经能提供足够高的反馈密度。
错误捷径是让同一个代理一边猜原系统怎么做,一边直接照着写,最后只凭“看起来像”验收。这条路短期很快,长期却最难纠错,因为事实、实现和评价标准已经搅在一起。真正省时间的不是少写一条线,而是让每条线都只解决自己最擅长、最容易核对的问题。
下次我会怎么做
下次遇到既要理解内部行为、又要做出可运行复刻的目标,我会先把工作拆成3份材料:证据清单、功能规格、重建义务。证据清单只写已经观察到的事实,并标注版本和输入条件;功能规格描述外部行为;重建义务则写清每一项实现要满足什么、怎样验证。
然后我会给分析线和建造线设清洁交接点。分析线不直接替建造线决定代码结构,建造线也不把暂时跑通的实现反过来当成原系统事实。每完成一段,就用同一组输入做接口回放,再对关键状态做视觉比对。结果不一致时,先归因,再补查,不用“全部重来”掩盖具体缺口。
最后,我会把未知保留下来。未知不是流程失败,未经证实的确定才是。一个可复用的重建流程,不是把原系统复制得越多越好,而是让每个结论都有证据、每项实现都有义务、每个差异都有回查路径。