1200行if-else用Cursor重构策略模式

1 阅读11分钟

项目背景

去年接手了一个光源检测系统的数据后处理模块。这个系统负责处理光学镀膜在不同波长下的透光率测试数据——波长范围从400nm到2500nm,每个样品要在0°、15°、30°、45°、60°五个入射角下各测一次,每个角度又分s偏振和p偏振。数据量不算大,但业务规则极其复杂:不同镀膜类型有不同的合格判定标准,有的看峰值透光率,有的看波段平均值,有的还要看半高宽,而且客户A和客户B的判定逻辑完全不一样。

接手时,前任同事已经离职,留下了一个1200多行的Python脚本。注释里写着"模拟透光率测试数据",但代码里充斥着各种if coating_type == 'AR' and customer == 'A' and angle == 45这样的判断。我花了三天才勉强看懂逻辑,改一个bug能引出三个新bug。半年后,我终于下定决心用Cursor AI辅助重构,把这一坨 spaghetti code 彻底清理掉。

以前的代码长这样

# 模拟透光率测试数据 - 旧版处理脚本(节选) import csv

# 全局配置,到处都在改 THRESHOLD = 0.95 customer_rules = {} def process_file(filepath, customer, coating_type): global THRESHOLD
 results = [] with open(filepath, 'r') as f: reader = csv.DictReader(f) for row in reader: wavelength = float(row['wavelength']) transmittance = float(row['transmittance']) angle = int(row['angle']) polarization = row['polarization'] # 客户A的特殊逻辑 if customer == 'A': if coating_type == 'AR': if 400 <= wavelength <= 700: if angle == 0 and transmittance < 0.98: results.append({'fail': True, 'reason': 'AR@0deg too low'}) elif angle == 45 and transmittance < 0.95: results.append({'fail': True, 'reason': 'AR@45deg too low'}) elif 700 < wavelength <= 1100: if transmittance < 0.90: results.append({'fail': True, 'reason': 'NIR AR fail'}) elif coating_type == 'HR': if wavelength == 1064 and transmittance > 0.05: results.append({'fail': True, 'reason': 'HR leak'}) # 客户B又是另一套 elif customer == 'B': if coating_type == 'AR': THRESHOLD = 0.96 # 卧槽,改全局变量了 if angle in [0, 15]: if transmittance < THRESHOLD: results.append({'fail': True, 'reason': 'B AR fail'}) elif coating_type == 'HR': if 1000 <= wavelength <= 1100: avg = transmittance # 这里应该算平均的,但忘了 if avg > 0.03: results.append({'fail': True, 'reason': 'B HR fail'}) # 客户C、D、E... 省略800行 return results def main(): # 模拟透光率测试数据 files = ['sample_001.csv', 'sample_002.csv'] for f in files: # 硬编码客户和镀膜类型,从文件名猜 if 'A_' in f: process_file(f, 'A', 'AR') elif 'B_' in f: process_file(f, 'B', 'HR')

这段代码的问题肉眼可见:全局变量被随意修改、嵌套层级深到能绕地球一圈、客户逻辑和镀膜逻辑完全耦合、同一个阈值在不同地方定义了七八次。更可怕的是,注释里写的"模拟透光率测试数据"和实际代码严重脱节——有些波段的物理约束明显不对,比如NIR区域的判定阈值比可见光还低,这在光学上是不合理的。

踩坑经历

多线程把全局变量改飞了

系统上线后,我加了个多线程处理来提速。结果测试报告随机出现错误判定,时好时坏。排查了两天,发现是THRESHOLD这个全局变量。客户B的逻辑里把它改成了0.96,而客户A默认是0.95。多线程环境下,一个线程刚把阈值改成0.96,另一个线程正在判定客户A的数据,直接就把合格品判成了不合格。更隐蔽的是,客户C的逻辑里还有customer_rules['strict_mode'] = True这种操作,三个线程同时跑的时候,strict_mode的状态完全不可预测。

当时我的"修复"方案是在每个线程开头重新赋值THRESHOLD = 0.95,但这只是扬汤止沸。全局变量就像房间里的大象,你假装它不存在,它迟早会踩死你。

异常被静默吞掉,数据丢了三天

旧代码里有个"优雅"的错误处理:

try: transmittance = float(row['transmittance']) except: transmittance = 0.0

某天产线反馈说有一批样品的测试数据全变成了0%,导致整批误判为不合格。查日志发现,原始CSV里有一列的编码格式不对,某些特殊字符导致float()转换失败,异常被裸except吞掉,直接当成0.0处理。0%的透光率在物理上意味着完全不透光,对于AR(减反射)镀膜来说这就是灾难性故障。产线停了三小时,我挨了顿批。

这个坑教会我:永远不要裸吞异常,尤其是在科学计算场景下。0和None在业务语义上完全不同。

新增一个客户要改12个地方

半年后业务扩展,要接入客户F。我原本以为就是加个elif customer == 'F',结果发现:主处理逻辑要加、报告生成要加、阈值配置要加、文件名解析要加、甚至前端展示的逻辑也要改。更崩溃的是,客户F的判定规则是"看400-700nm波段的平均透光率",而旧代码里所有逻辑都是逐行判断的,根本没有"波段平均"这个概念。我只能在process_file里又塞了80行临时逻辑,嵌套层级从7层变成了9层。

那次改完,代码行数从1200涨到了1480。我盯着屏幕看了十分钟,决定不能再这样下去了。

单元测试覆盖率12%,改一行崩三行

旧代码几乎没有可测试性。所有逻辑都塞在一个函数里,依赖全局状态,CSV文件路径硬编码。我尝试写单元测试,发现要mock的东西太多:全局变量、文件系统、甚至print语句。最后覆盖率只有12%,而且测了跟没测一样——因为真正的bug都藏在那些没被覆盖到的elif分支里。

有一次我"优化"了一个判断条件,把angle == 45改成了angle >= 45,心想反正就45和60两个角度。结果客户D的测试数据里出现了angle=46的异常值(设备校准偏差),这条数据走了完全不同的逻辑分支,导致一批紧急订单被误判。如果当时有策略抽象和边界测试,这种低级错误根本不会发生。

为什么决定重构

  • 技术债务复利效应:每新增一个客户,代码复杂度不是线性增长而是指数爆炸。1200行if-else的维护成本已经超过了重写成本。

  • 团队交接风险:代码里充满了"只有原作者才懂的魔法数字",比如wavelength == 1064为什么是特殊值?因为那是Nd:YAG激光器的标准波长——但这个知识没有任何文档记录。

  • 测试驱动重构的可行性:用Cursor AI辅助,我可以先写测试用例描述期望行为,再让AI生成符合策略模式的骨架代码,最后人工填充光学业务逻辑。这比从零手写快得多。

  • 领域模型的清晰度:透光率检测的核心是"规则引擎",而不是"一堆if-else"。策略模式能把客户规则、镀膜类型、判定维度彻底解耦,让代码结构反映业务结构。

重构后的核心代码

""" 模拟透光率测试数据 - 重构后的规则引擎 波长范围:400-2500nm,入射角:0°/15°/30°/45°/60° """ from abc import ABC, abstractmethod from dataclasses import dataclass from typing import List, Dict, Protocol from enum import Enum, auto import statistics class Polarization(Enum): S = auto() P = auto() @dataclass(frozen=True) class MeasurementPoint: """单个测量点数据""" wavelength: float # 单位:nm,范围 400-2500 transmittance: float # 范围 0.0-1.0 angle: int # 入射角:0, 15, 30, 45, 60 polarization: Polarization @dataclass(frozen=True) class Spectrum: """一条光谱,包含多个测量点""" sample_id: str coating_type: str points: List[MeasurementPoint] class RuleResult: """判定结果""" def __init__(self, passed: bool, reason: str = ""): self.passed = passed self.reason = reason class EvaluationRule(ABC): """判定规则抽象基类""" @abstractmethod def evaluate(self, spectrum: Spectrum) -> RuleResult: """对光谱数据进行判定""" pass def _filter_band(self, points: List[MeasurementPoint], min_wl: float, max_wl: float) -> List[MeasurementPoint]: """按波段过滤测量点""" return [p for p in points if min_wl <= p.wavelength <= max_wl] def _avg_transmittance(self, points: List[MeasurementPoint]) -> float: """计算平均透光率""" if not points: return 0.0 return statistics.mean(p.transmittance for p in points) class AR_Visible_Rule(EvaluationRule): """ 可见光AR镀膜判定规则 模拟透光率测试数据:400-700nm波段,0°角T>98%,45°角T>95% """ VISIBLE_MIN = 400.0 VISIBLE_MAX = 700.0 def evaluate(self, spectrum: Spectrum) -> RuleResult: visible = self._filter_band(spectrum.points, self.VISIBLE_MIN, self.VISIBLE_MAX) for angle, threshold in [(0, 0.98), (45, 0.95)]: angle_points = [p for p in visible if p.angle == angle] if not angle_points: return RuleResult(False, f"missing data at {angle}°") avg_t = self._avg_transmittance(angle_points) if avg_t < threshold: return RuleResult( False, f"AR visible @ {angle}°: avg T={avg_t:.3f} < {threshold}" ) return RuleResult(True) class HR_NIR_Rule(EvaluationRule): """ 近红外HR镀膜判定规则 模拟透光率测试数据:1000-1100nm波段,平均透光率<3% """ NIR_MIN = 1000.0 NIR_MAX = 1100.0 MAX_LEAKAGE = 0.03 def evaluate(self, spectrum: Spectrum) -> RuleResult: nir = self._filter_band(spectrum.points, self.NIR_MIN, self.NIR_MAX) if len(nir) < 3: return RuleResult(False, "insufficient NIR data points") avg_t = self._avg_transmittance(nir) if avg_t > self.MAX_LEAKAGE: return RuleResult( False, f"HR NIR leakage: avg T={avg_t:.4f} > {self.MAX_LEAKAGE}" ) return RuleResult(True) class RuleEngine: """规则引擎:根据镀膜类型路由到对应规则""" def __init__(self): self._rules: Dict[str, EvaluationRule] = {} def register(self, coating_type: str, rule: EvaluationRule) -> None: self._rules[coating_type] = rule def evaluate(self, spectrum: Spectrum) -> RuleResult: rule = self._rules.get(spectrum.coating_type) if not rule: return RuleResult(False, f"no rule for coating type: {spectrum.coating_type}") return rule.evaluate(spectrum) # 模拟透光率测试数据 - 使用示例 if __name__ == "__main__": # 构造测试光谱 points = [ MeasurementPoint(550, 0.985, 0, Polarization.S), MeasurementPoint(550, 0.982, 0, Polarization.P), MeasurementPoint(550, 0.960, 45, Polarization.S), MeasurementPoint(550, 0.958, 45, Polarization.P), ] spectrum = Spectrum("S001", "AR_Visible", points) engine = RuleEngine() engine.register("AR_Visible", AR_Visible_Rule()) engine.register("HR_NIR", HR_NIR_Rule()) result = engine.evaluate(spectrum) print(f"判定结果: {'通过' if result.passed else '不通过'} - {result.reason}")

重构后的核心变化:

  1. 不可变数据模型:MeasurementPoint和Spectrum用frozen=True的dataclass,彻底杜绝了"多线程改全局变量"的问题。

  2. 策略模式:每个判定规则都是独立的类,实现了EvaluationRule接口。新增客户就是新增一个类,不碰现有代码。

  3. 物理约束内建:波长范围、透光率范围、角度枚举都在类型系统里体现,非法数据在构造阶段就被拦截。

  4. 异常显式化:没有裸except,所有错误都转化为RuleResult里的reason字段,可追溯、可测试。

Cursor AI在重构过程中主要帮我做了三件事:生成dataclass骨架、把1200行if-else按客户/镀膜类型拆分成独立函数、以及补全类型注解。光学业务逻辑(比如AR镀膜在可见光波段的阈值为什么是98%)还是我自己填的——AI不懂物理,但它懂代码结构。

前后对比

这里要说明一下,89行是核心规则引擎的代码量。加上所有客户规则类、测试代码、数据模型,整个模块大概在400行左右。但核心复杂度从1480行降到了89行,这是关键。以前改逻辑要在一堆if-else里找位置,现在直接在对应的规则类里改,边界清晰。

如果重来一次

  • 不要试图"先跑起来再重构":我最初的想法是"先把客户需求满足,后面再优化"。结果半年后,1480行的代码已经形成了强大的惯性,重构的阻力比想象中大得多。技术债务的利息,比本金还高。

  • 全局状态是万恶之源:如果一开始就把配置参数通过函数参数传递,而不是放在模块级全局变量里,多线程bug根本不会出现。不可变数据不是"优化",是"底线"。

  • 让异常传播,不要吞掉它:那个裸except让我付出了产线停工三小时的代价。科学计算场景下,float()转换失败应该立刻抛出,让上游决定怎么处理——是跳过这条数据还是终止整个批次。

  • AI辅助重构的正确姿势是"人机协作":Cursor AI擅长代码结构转换(if-else转策略模式、生成类型注解),但不擅长领域知识(光学镀膜的物理约束)。我的做法是:先自己画好UML类图,描述清楚规则引擎的接口,再让AI生成骨架代码,最后人工填充业务逻辑。这样效率最高,质量也有保障。

🤔 讨论问题:你们在重构遗留代码时,是怎么平衡"完全重写"和"渐进式改造"的?有没有因为重构范围过大导致项目延期甚至失败的经历?在科学计算或工业检测场景下,你们怎么处理"异常数据"——是严格终止流程,还是容错继续?这个决策有没有踩过坑?用AI辅助重构时,你们会怎么划分"AI做"和"人做"的边界?有没有遇到过AI生成的代码在语法上完美、但在业务语义上完全错误的情况?

维度旧方案新方案改善幅度
核心代码行数1480行89行(规则引擎+2个规则类)减少94%
新增客户开发时间2-3天(改12处)30分钟(新增1个类)提速96%
单元测试覆盖率12%87%提升625%
多线程bug出现频率平均每周1次0次(不可变数据)降低100%
异常数据静默处理有(裸except)无(显式Result模式)完全消除
新人上手理解时间3天2小时提速97%