资讯动态

策略模式重构半导体可靠性测试机上位机:从switch-case到配置驱动

发布时间:2026/10/13 3:39:57 来源:尧图企业网站定制
做半导体可靠性测试机上位机那段时间我最怕听到的一句话就是“下周再加三种测试项”。不是因为写代码难而是主流程里那个巨大的 switch-case 已经被塞了几十个测试类型分支每次新增逻辑都要从几百行的流程代码里找到正确的位置小心翼翼地把新分支挂上去还得祈祷不要碰坏旁边正在跑的高温老化流程。后来我下定决心用策略模式Strategy Pattern把这套基于 .NET Core 的上位机系统重构成“策略即组件”的结构把不同的应力测试方案、设备通信协议、失效判定规则全部拆成独立策略运行时根据测试编码动态装配。重构完成之后系统连续稳定跑了好几个月新测试项基本只需要新增一个类、注册一行配置主流程完全不用动。这篇文章就围绕这次重构聊聊策略模式在半导体可靠性测试机上位机中的完整落地思路包括接口设计、依赖注入、运行时解析、组合策略、安全联动以及那些只有踩过坑才会知道的细节。适合正在做测试系统、工业上位机、或者想把设计模式真正用进业务的朋友参考。1. 先交代战场上位机系统那些“必须按批次切换”的变化点1.1 可靠性测试机上位机到底在管什么很多人一听“半导体可靠性测试”以为就是拿个万用表测一测。真实场景比这复杂得多。测试机通常由老化箱或温控腔体、多路电源、源测量单元SMU、采集板卡组成一次可以同时测试几十甚至几百颗器件。上位机软件的职责是根据配方控制温箱升温或降温按照测试项要求给器件施加偏压周期性采集漏电流、阈值电压、工作电流等电参数根据判据判断器件是否失效把数据入库并在异常时触发保护动作。这里天然存在大量变化点。同一台设备可能要执行 HTOL高温工作寿命、HTGB高温栅偏、THB高温高湿偏压、TC温度循环等不同测试项。不同客户、不同批次对失效定义不一样有的要求超限立即判废有的要求连续三次超限才判失效。温控设备可能来自不同供应商有走 Modbus RTU 的有走 Modbus TCP 的还有走私有帧协议的。这些“不同”放在同一个人上位机里就是一堆需要管理的变化。1.2 最初的代码为什么越改越重我接手的时候这套上位机的核心是一个叫作“流程管理器”的类里面除了主流程还有一个按测试代码分发的 switch-case每个 case 下面跟一大段执行逻辑。一开始只有两三种测试项这样写确实简单直接。但测试项慢慢增加到十几种以后问题就来了。首先是分支之间互相污染。某个流程里顺手改了公共的采样标志位另一个测试项运行时就踩到了。最典型的一次有人在 HTOL 分支里加了一个“先预热再开始采集”的逻辑结果 THB 测试跑到一半突然多了一段不明来源的等待时间查了半天才发现是共享变量在作怪。其次是主流程被改得没法读。升温、保温、采参、判定、数据落库这些步骤和具体的测试项逻辑紧紧耦合在一起想抽出公共框架根本无从下手。最要命的是回归验证范围越来越大每次改动都感觉全系统随时可能崩。这类问题的本质是算法族之间没有隔离。“测试执行方案”作为一个整体被按测试类型硬切成几十个分支每加一种方案就要改主流程。当变化维度和变化频率都明显时就该考虑用策略模式了。1.3 为什么是策略模式而不是模板方法或者观察者当时也有同事建议用模板方法把“升温-保温-施压-采集-降温-判级”作为固定骨架让子类去重写每一步。模板方法确实可以解决一部分问题但实际操作中不同测试项的骨架差异比想象中大。有的测试项需要先做预处理有的中间要切换温区有的数据采集是连续采样有的只是定时快照。强行把它们套进同一个骨架要么抽象类里堆满空实现要么就会冒出大量钩子方法。策略模式的好处在于它把整套执行方案当作一个可替换的算法族。一个测试项对应一个策略对象运行时按需取用并不强制所有策略共享同一个骨架。执行方案内部到底怎么组织是策略自己的事。至于有没有公共步骤完全可以通过后续引入模板方法去优化这叫策略模式和非模板方法是组合关系而不是替代关系。观察者模式当时也被提到但我们需要的不是“状态改变时通知多个对象”而是“针对不同情况选择不同算法”显然策略模式更贴合。说白了选择模式的依据是变化点长什么样策略模式解决的是“同一件事有不同做法、希望运行时自由切换”的问题这正是测试机上位机最真实的需求。2. 先把领域语言翻译成接口测试项、上下文、策略边界的第一次划定2.1 IStressTestStrategy 接口的取舍重构第一步不是写实现而是定义策略接口。我当时定义的接口非常简单直接就动手了public interface IStressTestStrategy { string Name { get; } TaskStressResult ExecuteAsync(StressContext context, CancellationToken cancellationToken); }Name 用来标识一个策略对应配方表里的测试编码ExecuteAsync 接收上下文和取消令牌返回测试结果。之所以用异步方法是因为实际上位机里所有设备操作都是 IO 密集型温控、源表读写、通信都要异步执行否则 UI 线程或者工位调度线程会被卡死。方法签名没有设计成一长串参数而是传一个 StressContext 对象。这是很关键的一个设计决策。测试项执行过程中要接触的东西太多了配方参数、温控设备代理、电源代理、采样器、数据收集器、日志服务。如果全部作为参数传进去接口会非常脆弱每次增加一个依赖都要改所有实现策略模式反而成了维护负担。用一个上下文对象把这些运行时依赖装起来接口就稳定了。2.2 StressContext 应该装什么、不应该装什么我设计的 StressContext 大致长这样public sealed class StressContext { public TestRecipe Recipe { get; set; } public IChamberProtocolStrategy Chamber { get; set; } public IPowerSupplyStrategy PowerSupply { get; set; } public ISamplingStrategy Measurer { get; set; } public IFailureJudgementStrategy Judgement { get; set; } public IDataCollector Collector { get; set; } public ILogger Logger { get; set; } }每个工位在执行测试之前都会先创建一个独立的 StressContext把当前工位的配方和设备代理放进去。这样不同工位之间天然隔离策略对象本身保持无状态。这里有一个很重要的原则上下文可以放服务接口但不要把 IServiceProvider 塞进去。一旦上下文变成服务定位器策略内部就会隐藏大量依赖关系测试和排错都会变得困难。我在重构初期犯过这个错误某个策略直接从上下文里取出服务容器自己解析数据库服务导致单元测试完全没法做后来全部改成了显式属性。StressResult 也是重构中专门定义的返回类型包含测试项编码、开始结束时间、是否通过、采样摘要、失效信息。这里要避免让策略直接返回裸布尔值因为上位机后续要追溯完整的测试过程失败原因、关键样本、耗时指标都很有价值。2.3 策略模式和模板方法在这个场景下的结合虽然前面说了模板方法不适合作为主结构但它可以成为某个策略内部的实现手段。举个例子我后来发现很多测试项的流程轮廓确实是一样的配置参数、执行老化过程、采集数据、清理设备。于是我为这类策略写了一个抽象基类public abstract class BaseStressStrategy : IStressTestStrategy { public abstract string Name { get; } public async TaskStressResult ExecuteAsync(StressContext context, CancellationToken ct) { await ConfigureAsync(context, ct); await RunAsync(context, ct); await CollectAsync(context, ct); await CleanupAsync(context, ct); return BuildResult(context); } protected abstract Task ConfigureAsync(StressContext context, CancellationToken ct); protected abstract Task RunAsync(StressContext context, CancellationToken ct); protected virtual Task CollectAsync(StressContext context, CancellationToken ct) Task.CompletedTask; protected virtual Task CleanupAsync(StressContext context, CancellationToken ct) Task.CompletedTask; protected virtual StressResult BuildResult(StressContext context) new StressResult(); }这样做的好处是策略接口作为统一入口保持不变框架层只依赖接口具体策略内部可以用模板方法管理流程子类只需要实现差异化步骤。当时我为了避免策略类里出现大段重复代码才补了这层抽象。后来维护起来确实舒服很多不是所有策略都需要继承这个基类比如某些特殊测试项根本没有 Configure 和 Collect 阶段直接实现接口就行。3. .NET Core依赖注入下的策略注册与运行时切换从 switch-case 到配置驱动3.1 策略的注册方式和生命周期怎么选.NET Core 自带的依赖注入容器天然适合做策略管理。把所有策略实现注册为同一接口的不同实现再用一个工厂类负责按名称解析switch-case 就可以彻底删除。注册代码很简单services.AddScopedIStressTestStrategy, HtolStressStrategy(); services.AddScopedIStressTestStrategy, HtgbStressStrategy(); services.AddScopedIStressTestStrategy, ThbStressStrategy();这里我特别想提醒一点策略的生命周期选择会直接影响系统稳定性。最安全的做法是把策略注册为 Transient 或 Scoped然后通过 IServiceProvider 在运行时解析保证每个工位拿到的是独立的策略实例。如果把策略注册为 Singleton同时策略内部又存了可变状态那多个工位并发跑测试时会互相污染出现漏电流数据串到别的工位这种严重问题。一开始我还真在工厂里用构造函数注入了一堆具体策略比如public StressStrategyFactory(HtolStressStrategy htol, HtgbStressStrategy htgb, ...)这种写法的问题有三个策略数量多了以后构造函数会变得不可维护工厂和具体策略强耦合失去了“新增策略不动现有代码”的意义如果以后想用配置文件驱动策略解析这种写法完全做不到。所以工厂必须依赖 IServiceProvider。3.2 用策略工厂替代 switch-case 的具体实现我实现的工厂类大致是这样public interface IStressStrategyFactory { IStressTestStrategy GetStrategy(string testCode); } public sealed class StressStrategyFactory : IStressStrategyFactory { private readonly IServiceProvider _serviceProvider; private readonly IReadOnlyDictionarystring, Type _strategyMap; public StressStrategyFactory(IServiceProvider serviceProvider) { _serviceProvider serviceProvider; _strategyMap new Dictionarystring, Type(StringComparer.OrdinalIgnoreCase) { [HTOL_125C_10V] typeof(HtolStressStrategy), [HTGB_150C_20V] typeof(HtgbStressStrategy), [THB_85C_85RH] typeof(ThbStressStrategy) }; } public IStressTestStrategy GetStrategy(string testCode) { if (!_strategyMap.TryGetValue(testCode, out var type)) { throw new KeyNotFoundException($找不到测试策略: {testCode}); } return (IStressTestStrategy)_serviceProvider.GetRequiredService(type); } }工厂在启动时构建一个“测试编码到实现类型”的字典。运行时传入的 testCode 来自配方数据查字典找到类型再从容器解析实例。这样做的好处是新增策略根本不需要修改工厂类和主流程只需要在注册代码里加一行并在工厂的映射字典里加一条记录。如果嫌字典里的映射太死板还可以用命名约定策略类的名称本身就带有测试编码前缀或者通过特性标注。比如[StrategyCode(HTOL_125C_10V)] public sealed class HtolStressStrategy : IStressTestStrategy然后利用反射在程序集里扫描所有带 StrategyCodeAttribute 的策略类自动注册并建映射。这样连工厂的字典都不用手动维护了。我在项目里最终用了反射扫描当时完全是为了减少新策略上线时的遗漏风险。反射扫描省事但也会让代码可读性下降第一次做的话建议先把字典方式跑通再加扫描。3.3 从数据库配置到策略实例的完整链路策略模式真正的价值在运行时切换。体现在这套系统里就是“配方驱动策略”。配方表里有个测试编码字段比如 HTOL_125C_10V上位机调度线程读取配方后会把这个编码传给 StressStrategyFactory工厂解析出对应策略再交给工位执行。伪代码如下public async Task RunTestAsync(int stationId, TestRecipe recipe, CancellationToken ct) { var context BuildContextForStation(stationId, recipe); var strategy _strategyFactory.GetStrategy(recipe.TestCode); var result await strategy.ExecuteAsync(context, ct); await _resultStore.SaveAsync(stationId, result, ct); }现场调试时工程师只需要在数据库里把某个工位的配方改成新的测试编码不需要停掉整个系统再重新编译下一次调度就会自动加载新策略。这种体验和使用旧的 if-else 流程完全不同改动风险从“改主流程影响所有测试项”下降到了“只影响新增的那个策略类”。4. 从单个策略到一组策略协议适配、采集、存储的拆分与组合4.1 设备协议适配器为什么要做成策略上位机对接的设备五花八门温控器是重灾区。有的设备用 Modbus RTU有的用 Modbus TCP还有的自己定义帧格式。最开始我把设备驱动写成一套类内部用协议类型字段分支处理结果每加一台设备就要修改驱动类而且测试环境里没有实物设备时根本没法验证。后来我抽了一个接口public interface IChamberProtocolStrategy { Task ConnectAsync(CancellationToken ct); Taskdouble ReadTemperatureAsync(CancellationToken ct); Task SetTemperatureAsync(double target, CancellationToken ct); Task StartRampAsync(double target, double rate, CancellationToken ct); }每个协议对应一个实现。Modbus RTU 一个类Modbus TCP 一个类私有协议一个类。StressContext 里的 Chamber 属性就是这个接口类型。这样测试策略本身完全不需要关心温控器到底是串口还是网口连的只调用接口方法就行。新接入一台设备等于新加一个协议策略主流程和测试策略都不动。这个改动让现场接入新设备的时间从一两周缩短到一两天主要工作变成了纯通信调试。4.2 数据采集和存储策略的异步组合数据采集也是个明显的策略点。不同测试项对采样频率的要求差别很大。有些高温老化测试每五分钟采一次参数就行有些动态测试需要毫秒级连续采集。于是我把“怎么采”抽成了 ISamplingStrategypublic interface ISamplingStrategy { TaskSampleBatch SampleAsync(StressContext context, CancellationToken ct); }连续采集和定时快照就是两个不同的实现。存储也一样。一开始所有采样点都实时写数据库工位一多数据库压力非常大写库经常成为瓶颈。我抽了 IDataCollector 接口实现分为直写数据库和先写本地缓冲、批量落库两种模式。测试策略里只调用 collector.Add(sample)具体是立刻入库还是攒一批再入库由数据收集策略自己决定。后来压力测试发现批量落库模式更合适我直接改配置切换存储策略测试流程代码一行都没动。4.3 组合策略时最容易搞乱的依赖方向单个策略好写策略组合起来容易乱。比如 HTOL 策略执行时要看温控策略要用采样策略还要用判定策略。一种做法是把这些依赖全部塞进 StressContext让策略自己取这会导致上下文膨胀。另一种做法是在工厂里组装策略之间只通过构造函数传递所需子策略public sealed class HtolStressStrategy : IStressTestStrategy { private readonly IChamberProtocolStrategy _chamber; private readonly ISamplingStrategy _sampling; private readonly IFailureJudgementStrategy _judgement; public HtolStressStrategy(IChamberProtocolStrategy chamber, ISamplingStrategy sampling, IFailureJudgementStrategy judgement) { _chamber chamber; _sampling sampling; _judgement judgement; } }不过要注意这种方式必须保证所有子策略都注册在 DI 容器里。而且组装关系一旦写在构造函数里就要求策略之间相对稳定不能随便变。如果组合关系频繁变动建议用抽象工厂或者构建器模式来管理。我在实际项目中把“判断哪些子策略组合在一起”的逻辑放在了专门的策略组装器里Htol 使用 Htol 组合Htgb 使用 Htgb 组合这样既清晰又方便测试。组装配器是工厂模式的延伸和策略模式并不冲突反而让整套结构更完整。对于中小型项目来说直接通过 DI 和构造函数组装已经够用了组装器可以等组合数量确实多起来再引入。5. 失效判定与安全联动的策略化保护通道必须单独走5.1 三种失效判定策略的设计思路失效判定是可靠性测试机最核心的业务逻辑之一。判据通常包括漏电流上限、阈值电压漂移范围、工作电流偏差等等。但不同客户对“失效”的定义完全不同有的要求某一项超限就立即判废有的要求连续三次采样都超限才算失效还有的要求看趋势比如漏电流在若干小时内持续抬升也要报失效即使绝对值还没超限。针对这些差异我定义了判定策略接口public interface IFailureJudgementStrategy { FailureVerdict Check(SampleRecord record, LimitRule rule, IReadOnlyListSampleRecord history); }实现了三种策略固定阈值判定、滑动窗口趋势判定、复合判定。固定阈值最简单超了就判废。趋势判定会拿最近 N 个样本做线性拟合斜率超阈值就报警。复合判定把多个判据组合起来按优先级返回最终结论。测试策略每采到一个样本就把当前记录和历史数据交给判定策略由策略决定继续测、判废、还是仅记录警告。5.2 保护联动为什么不能当成普通策略上位机还有一条不能乱的线安全保护。过温、过流、通信中断这些情况属于保护通道必须立刻响应不能等技术任的某个采集循环跑完才被发现。我单独定义了保护策略接口并让它在执行时拥有更高的优先级public interface IProtectionStrategy { int Priority { get; } Task ExecuteAsync(StressContext context, ProtectionTrigger trigger, CancellationToken ct); }保护策略包括超温断电、过流保护停机、通信中断后的安全归位等。它和业务测试策略是两条执行线。当监控线程发现温箱温度超出安全区间会直接触发保护策略立即切断对应工位的电源施加而不是等着正在执行的测试策略自己处理。这个设计是因为曾经踩过真实教训某个策略在长时间循环里等待设备返回数据温度已经超过上限却没人干预后来把保护逻辑独立出来才解决。5.3 多工位并发下的状态隔离和实例安全问题上位机最重要的并发场景是多工位同时测试。不同工位跑不同策略每个工位有独立的采样数据。如果策略实例被多个工位共享而且策略内部保存了“当前样本列表”之类的状态那就会出现数据串线。实际中就发生过工位 A 的漏电流曲线突然混进工位 B 的数据排查了两天才定位到是某个策略类里意外写了一个实例字段保存了上一个工位遗留的采样数组。解决方案其实很简单策略类保持无状态所有运行期数据都放到 StressContext 里。每个工位创建自己的 StressContext策略实例从 DI 按作用域解析或者通过工厂每次都返回新实例。只要做到“状态跟着工位走策略只是算法”并发问题就会消失。这一点听起来容易执行时却很容易破戒我在代码评审时经常提醒团队看到策略类里出现非只读实例字段就要认真检查是不是有状态泄漏。另外一个细节是保护策略通常要注册成 Singleton因为保护逻辑无状态而且需要全局共享比如某个设备级别的过温保护一个信号要影响多个工位。如果保护策略的作用域搞错了反而会漏掉联动。6. 重构后的收益清单与让人血压升高的踩坑清单6.1 重构前后的直观对比重构这套上位机系统我最直观的感受是“改代码不害怕了”。把主流程里的 switch-case 移除、把测试项按策略拆分之后主流程代码量大约减少了百分之七八十剩下的主要是任务调度、上下文创建和结果处理。新增测试项的模式从“在长函数里插一段逻辑”变成了“新增类注册一行配置”改动点非常集中。项目重构前重构后新增一种测试项修改主流程类、修改 switch-case、验证其他测试项回归新增策略类、注册一行、配置测试编码主流程代码量数百行的单一大类只保留调度和上下文构建测试项之间耦合共享变量互相影响策略独立状态隔离在上下文新设备接入修改设备驱动类新增协议策略回归范围全流程回归只验证新增策略和涉及工位定位问题难度在长函数里翻逻辑直接定位具体策略类表格里的数字是近似值但方向非常明确。真正让我觉得值得的是后来新增了三个测试项整个改动只花了一个下午这在重构之前不敢想。6.2 策略化过程中最容易踩的坑踩坑清单大概是这些第一策略对象里悄悄存了状态导致并发工位数据串线防不胜防第二工厂通过构造函数注入具体策略新增策略时需要改工厂等于没有解除耦合第三把全部代码都策略化哪怕只有一个实现的“未来可能变化”也硬造一个接口类数量飞速膨胀反而不好维护第四异常处理散落在策略内部监控日志不统一故障排查困难第五策略编码拼写不一致数据库里写错一个字符就抛 KeyNotFoundException生产环境没有好的错误提示会很崩溃第六没有给策略执行加超时控制某台设备突然卡死整个工位任务就会悬挂到天荒地老。针对第五点我后来加了启动时的策略映射预校验系统启动时扫描所有注册的策略名称和数据库里用到的测试编码比对不匹配直接报错阻止启动避免跑了一半才发现策略不存在。第六点则是用 CancellationTokenSource 的 WithTimeout 包装策略执行超时就触发保护流程并向外抛出超时异常由调度层处理。6.3 策略模式适合用在哪一类变化点上总结一句话策略模式适合用在“同一目标有不同的实现路径而且需要在运行时动态选择”的变化点上。测量测试项的执行方案是设备协议适配是失效判据是存储策略也是。但不适合因为“以后可能变化”去给所有类套接口这种过度设计会给维护者带来额外的理解成本。我的建议是当第二次出现类似的变化需求时再把那个变化点抽象成策略第一次可以先用一个简单的 if第二次再重构也不晚。测试机这类系统设备越来越杂、测试项越来越多变化点很容易观察出来所以策略模式在里面天然有价值。还有一个很实际的小建议策略命名要用可读的字符串编码比如 HTOL_125C_10V而不是枚举或者数字。字符串编码可以直接存在配方表里现场工程师看着配置就能知道当前工位在跑什么测试。而用枚举的话新增策略就要改枚举类型还得重新编译完全违背了配置驱动的初衷。我们后来把策略名往往直接作为配方表字段新测试项上线时只在数据库里填新编码系统就能自动解析到新策略这一点在调试现场非常受用。如果让我重新来一遍这套重构我会先把变化点清单画出来再动手写接口。策略模式本身不复杂复杂的是找到正确的边界。现在这套系统最让我满意的不是类数量从少变多也不是 UML 图画得漂亮而是新增测试项不再心惊肉跳。调试设备、分析数据、处理异常的时候我能很快定位到对应的策略类而不是在那个几百行的主流程里上下翻找。最后再分享一个经验写完策略工厂之后建议在界面上留一个“策略预览”入口输入测试编码就能看到该策略会用哪些设备协议、哪种采样方式、哪套失效判定规则这个功能在现场联调时能省下一大半沟通成本。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑