
我先坦白一个判断但凡做过半导体测试系统上位机的人看到“策略模式”四个字多半会先愣一下——因为这活儿听起来太“业务重、学问深”了怎么和设计模式扯上关系但实际上我恰恰是在一个可靠性测试机上位机项目里才真正把策略模式用得服服帖帖的。原因很简单可靠性测试的场景太杂了。高温工作寿命、高温高湿偏压、温度循环、早期失效筛选每种测试方法对设备控制、数据采集、判定规则的要求都不一样而它们又共享同一套上位机框架。如果不用策略模式隔离变化switch-case会写到你怀疑人生。这篇文章就围绕这个真实场景展开讲讲我在 .NET Core 环境下如何把策略模式落到半导体可靠性测试机上位机的设备控制层、测试执行层和数据处理层包括接口设计、依赖注入整合、工厂选型、硬件适配和排查思路。适合正在做测试设备上位机、工控软件或者想了解策略模式工程化落地的开发朋友参考。1. 可靠性测试的业务盘点和模式切入点1.1 上位机系统要处理的测试类型半导体器件在量产流入市场之前必须做可靠性验证。常见的测试类型包括但不限于HTOL高温工作寿命测试让器件在高温下持续加电工作模拟多年使用损耗。HAST高加速温湿度偏压测试在高温高湿加偏压条件下加速腐蚀失效。TCT/TC温度循环测试让器件在低温、高温之间反复循环考验封装和互连的机械应力承受能力。每一类测试乍看都是“设置条件→跑流程→采集数据→判定结果”但具体到设备控制就很不一样HTOL 的主控对象是高温试验箱和直流电源需要精确控温、按时序加电、周期性地测静态参数。HAST 的主控对象是高压温湿度箱关键是湿度控制和偏压时序数据采集频率不高但安全联锁复杂。TC 的核心是温度循环曲线上位机要驱动温箱按斜率升降温并在每个温度驻留点触发电测。这意味着什么如果上位机只有一个“RunTest”方法内部靠 switch(type) 分发到不同逻辑那么每新增一种测试这段代码就会继续膨胀而且测试类型之间其实是不同的“算法族”不应该塞在一个类里。1.2 设备层的天然差异更麻烦的是设备层。同一台可靠性测试台可能同时挂着高温试验箱走串口、Modbus RTU直流电源走GPIB、IEEE-488数据采集卡走PCIe或LAN可编程负载走TCP/IP、SCPI命令不同品牌、不同型号的同类设备命令格式、参数单位、回读格式可能完全不同。有人说“我们全部用厂商SDK”但现实是同一实验室里旧设备和新型号并存谁也不敢保证所有设备都提供了统一的SDK就算有SDK的API风格也千差万别。所以在我接手这个项目时最大的痛点其实是上位机代码里到处是“if (设备型号 X) 发一条命令 else 发另一条命令”。这种代码不是不能跑而是每一次换设备、加测试项都会牵动一大片代码。策略模式正好切中这两个痛点把“做什么”和“怎么做”分离。需要变更的算法族各自封装成独立策略类调用方只依赖抽象接口运行时通过配置或工厂选择具体策略。2. 策略模式在测试执行层的落地实现2.1 抽象策略接口怎么设计我先从测试执行层说起。这是策略模式用得最直观的一层。我定义了一个策略接口专门描述“一种可靠性测试的执行算法”public interface IReliabilityTestStrategy { string TestType { get; } TaskTestPlan BuildPlanAsync(TestSpecification spec, CancellationToken ct); TaskTestResult ExecuteAsync(TestPlan plan, ITestContext context, CancellationToken ct); }三个方法对应三个稳定阶段BuildPlanAsync根据测试规范温度、电压、时长、循环次数等生成测试计划。ExecuteAsync按计划驱动设备执行测试期间持续采集数据。属性 TestType标记当前策略处理的测试类型便于工厂识别。设计时有个细节值得提我没有把解析规范、生成计划、执行测试全塞在一个方法里而是拆成 Build 和 Execute 两步。原因是可靠性测试的“计划”经常需要预先评审——操作员要先看生成的测试计划参数对不对确认后才真正跑。拆开后策略模式不仅管理执行算法也管理计划生成算法扩展性更好。2.2 具体策略类示例以 HTOL 为例具体策略大致长这样public class HtolTestStrategy : IReliabilityTestStrategy { private readonly ITemperatureChamber _chamber; private readonly IPowerSupply _powerSupply; private readonly IElectricalMeasurement _measurement; public string TestType HTOL; public HtolTestStrategy( ITemperatureChamber chamber, IPowerSupply powerSupply, IElectricalMeasurement measurement) { _chamber chamber; _powerSupply powerSupply; _measurement measurement; } public async TaskTestPlan BuildPlanAsync(TestSpecification spec, CancellationToken ct) { // 将 spec 转换为分阶段计划升温段、稳定段、加电偏压段、定期检测段 // 每个阶段包含预期值、允许偏差、持续时间 var plan new TestPlan(spec); plan.AddStage(new Stage(RampUp) { TargetTemp spec.Temperature, Duration TimeSpan.FromMinutes(30) }); plan.AddStage(new Stage(Soak) { TargetTemp spec.Temperature, Duration spec.SoakTime }); plan.AddStage(new Stage(Bias) { BiasVoltage spec.BiasVoltage, Duration spec.BiasDuration }); return plan; } public async TaskTestResult ExecuteAsync(TestPlan plan, ITestContext context, CancellationToken ct) { // 实实在在的执行逻辑驱动温箱、电源、测量单元 // 并采集关键参数写入上下文 } }HAST 和 TC 策略类结构类似只是内部调用的设备和流程编排不同。很多初学者会把具体策略类写得非常臃肿把设备通信、日志、数据库写入全部堆进去。我在第一版也犯过这个错后来意识到策略类不应该关心“怎么给设备发命令”和“怎么存数据”它更接近一个编舞者Choreographer负责编排设备的动作顺序和判断条件。设备通信细节应该下沉到硬件适配层数据持久化应该走独立的仓储服务。策略类只做流程控制。2.3 调用方的策略上下文有了多个策略类之后调用方比如测试调度器不能直接 new 具体策略需要通过一个工厂或选择器来拿到合适策略。这里我用了一个简单的策略选择器public class ReliabilityTestStrategySelector { private readonly IEnumerableIReliabilityTestStrategy _strategies; public ReliabilityTestStrategySelector(IEnumerableIReliabilityTestStrategy strategies) { _strategies strategies; } public IReliabilityTestStrategy Resolve(string testType) { return _strategies.FirstOrDefault(s string.Equals(s.TestType, testType, StringComparison.OrdinalIgnoreCase)) ?? throw new NotSupportedException($不支持的测试类型: {testType}); } }这个选择器配合 .NET Core 的依赖注入容器注册所有策略实例后按 TestType 自动匹配。这样新增测试类型时只要新增一个策略类并注册到容器调度器的代码完全不用改。开闭原则算是落到了实处。3. 与 .NET Core 依赖注入整合的工程化实践3.1 依赖注入的组织方式我们平时说“策略模式”很多时候只关心类和接口但真正让它变得好用的是容器和组织方式。我的做法是在 Startup 或 Program.cs 里统一注册builder.Services.AddSingletonHTOLTestStrategy(); builder.Services.AddSingletonHASTTestStrategy(); builder.Services.AddSingletonTCTTestStrategy(); builder.Services.AddSingletonReliabilityTestStrategySelector();这里我特意用 Singleton 而不是 Scoped 或 Transient原因是可靠性测试策略本身是无状态的——它的依赖设备适配器、日志服务是单例策略内部不持有“某一次测试的临时数据”临时状态都放在 TestContext 对象里传递。如果策略本身持有状态多线程跑多个测试批次时会互相污染。有人可能会问既然策略是无状态的为什么不直接用 Funcstring, IReliabilityTestStrategy 的注册方式当然也可以。但在大项目里显式注册每个策略类配合选择器代码更透明出问题也好排查。这个属于风格选择团队一致就好。3.2 注册时如何避免漏注册这个项目规模上来后策略类多了最尴尬的问题是新增一个策略类忘了注册结果运行时找不到报 NotSupportedException还得翻代码排查。我后来用了一种相对无痛的注册方式用反射按接口批量注册var strategyTypes typeof(HtolTestStrategy).Assembly .GetTypes() .Where(t !t.IsAbstract typeof(IReliabilityTestStrategy).IsAssignableFrom(t)); foreach (var type in strategyTypes) { builder.Services.AddSingleton(typeof(IReliabilityTestStrategy), type); }这样以后新增策略类放到对应程序集里自动注册少了一个手工维护点。需要注意如果项目里策略类分布在多个程序集要分别扫描。我当时的做法是抽了一个定义策略接口的类库策略实现也基本在一个类库里扫描一次就够。这种方式有个小坑如果两个策略类因为名字或归属程序集被扫描到多次可能重复注册。依赖注入容器默认后者覆盖前者但如果是意外重复容易困惑。所以我会给策略类加一个特性标记只扫描带标记的类[AttributeUsage(AttributeTargets.Class)] public sealed class ReliabilityTestStrategyAttribute : Attribute { public string TestType { get; } public ReliabilityTestStrategyAttribute(string testType) TestType testType; }注册时同时校验特性中的 TestType 与类的 TestType 属性一致不一致直接启动时抛异常而不是等到运行时才炸。3.3 基于配置的策略选择除了用 TestType 字符串选择策略我还支持了“按配置项”选择。比如一个上位机可能配套多种测试规范规范文件里有“TestMethod”字段取出来就是策略键。我让 TestPlan 对象带上 TestMethod 字段调度器直接读取并交给选择器public async TaskTestResult RunTestAsync(string specFilePath, CancellationToken ct) { var spec await _specParser.ParseAsync(specFilePath, ct); var strategy _strategySelector.Resolve(spec.TestMethod); var plan await strategy.BuildPlanAsync(spec, ct); return await strategy.ExecuteAsync(plan, _contextFactory.Create(), ct); }这样做的好处是现场工程师只需要修改测试规范文件上位机就能自动选择对应的测试流程不用重新编译部署上位机。对测试现场来说这个能力很实用。4. 硬件交互层的策略化改造4.1 设备驱动程序为什么要策略化可以说测试执行层的策略化解决了“测试算法”的变化问题而硬件交互层的策略化解决的是“设备型号”的变化问题。设备驱动如果每个型号写一个类上层调用时还是逃不掉 if-else。我在项目中定义了一组设备抽象接口public interface IChamberDriver { Task SetTemperatureAsync(double target, CancellationToken ct); Taskdouble ReadTemperatureAsync(CancellationToken ct); Task SetHumidityAsync(double target, CancellationToken ct); Task WaitForStableAsync(double tolerance, TimeSpan timeout, CancellationToken ct); }然后给不同厂商的温箱写各自的实现类串口型温箱驱动走 Modbus RTU内部把温度值转换成寄存器数据写完保持寄存器再轮询读取当前值。LAN 型温箱驱动走 TCP/IP 加 SCPI 命令内部需要处理连接建立、命令分隔、超时重试。这些驱动类本质上就是“完成同一组操作的不同算法”完全适合策略模式。4.2 驱动策略的工厂和适配为了避免上层到处 new 具体驱动我引入了驱动工厂public interface IChamberDriverFactory { IChamberDriver Create(ChamberConfig config); }工厂根据配置中的通信协议类型返回对应驱动实例。比如public IChamberDriver Create(ChamberConfig config) { return config.Protocol switch { ModbusRTU new ModbusRtuChamberDriver(config), SCPI-TCP new ScliTcpChamberDriver(config), _ throw new NotSupportedException($未知的温箱协议: {config.Protocol}) }; }这里其实是“策略选择”的另一个入口。有人会问驱动策略的切换是不是也可以用 DI 容器注册当然可以但设备驱动通常是按通道、按地址创建的实例配置参数每个通道不一样不适合直接作为单例挂在容器里工厂模式更合适。4.3 驱动的状态和生命周期问题说到驱动必须提醒一个常见问题设备驱动是有状态的——它持有串口端口、TCP 连接或 GPIB 句柄。这个状态必须和策略类的无状态特性区分开。我建议的策略是策略类IReliabilityTestStrategy无状态单例。驱动类IChamberDriver 等维护连接资源不跨测试共享则按测试会话创建保存为 TestContext 的一部分。我用了一个 DeviceSession 对象来承载一批测试过程中用到的所有驱动实例。测试结束或异常退出的 finally 里释放连接。这样避免策略单例持有驱动引用导致连接泄漏。5. 数据采集与解析策略容易被忽略的“第二个策略点”5.1 不同测试的数据形态差异可靠性测试上位机不只是发命令、收状态还得采集大量数据HTOL 测试过程需要定期记录每颗器件在偏压下的漏电流、电压降。HAST 测试更关心箱体湿度、温度、偏压时序的联动数据。TC 测试则侧重温度曲线升降温斜率、驻留时间和电阻漂移。这些数据在日志文件里的格式又不一样有的是 CSV有的是厂商自定义的文本格式有的是数据库表结构。如果上位机里只有一两个解析器那还好办一旦测试类型多了数据解析又是一个大坑。5.2 用策略模式做格式解析我定义了一个数据解析策略接口public interface ITestDataParser { string Format { get; } TaskIReadOnlyListTestDataPoint ParseAsync(Stream dataStream, CancellationToken ct); }每种测试格式写一个 Parser 类。选择方式与测试执行策略一致通过 Format 字段匹配。这样数据采集模块收到结果后先按格式选解析器再解析入库互不干扰。这里有个小技巧解析器内部不要直接操作数据库而是返回内存里的 DataPoint 列表由上层仓储统一落库。这样解析策略可以单独做单元测试不需要挂数据库测试速度快得多。5.3 数据合并的边界当多种策略数据需要合并展示时比如把温度曲线、电压曲线、漏电流曲线叠加到一张图上从不同 Parser 出来的数据结构需要统一。我在接口里强制所有解析器返回统一的 TestDataPoint 类型public sealed record TestDataPoint( DateTime Timestamp, string ChannelId, string Metric, double Value, string Unit);统一结构带来的好处是上层图表、报表模块完全不需要关心底层数据来源是哪个测试类型。这也是策略模式“面向抽象编程”的直接收益之一。6. 常见问题与排查技巧实录6.1 策略类误用状态导致数据串扰我第一次交付 HTOL 和 HAST 两个策略后测试现场反馈两台测试台同时跑的时候A 台的数据偶尔出现在 B 台的记录里。排查后发现原因是我在 HTOL 策略类里加了一个“当前测试批次号”字段想着方便日志里标记。结果策略是单例两个批次共用同一个实例批次号就被覆盖了。修复方式很彻底策略类里不允许保存任何测试相关状态全部状态挪到 TestContext。稍微改进一下就是定义策略实例时坚持纯函数式思想输入对象输出对象不要写 this.xxx。6.2 DI 容器里多个实现类的注册顺序坑用反射批量注册策略时如果策略类在扫描的程序集里有多层继承可能会把中间抽象层也注册进去。我踩过这个坑——某个中间基类也实现了接口但不是完整策略运行到它时很多依赖没注入直接 OOM 或空引用。后来我在扫描条件里加上了!t.IsAbstract并要求具体策略类必须标注特性属于哪类测试必须显式声明。启动时增加一个自检方法注册完所有策略后遍历特性的 TestType确认能找到唯一匹配的策略找不到直接抛异常启动失败。这比运行到一半才发现好得多。6.3 策略选择匹配不上时的降级方案真实场景里测试规范文件可能是旧版包含一个已经废弃的测试类型代码或者现场工程师拼写有误。如果 Resolve 直接抛 NotSupportedException整个测试调度就会中断。我后来加了一个降级策略完全匹配优先。无匹配时尝试匹配“别名列表”比如“HTOL”也可以缩写为“H”。仍然无匹配记录警告日志并返回一个“空操作策略”只记录数据不执行设备动作。这样现场不会因为写错一个字段导致整台机停摆警告日志又能帮助工程师发现配置问题。这个降级思路并不是策略模式本身要求的但在工程上很实用。6.4 设备通信超时影响的策略隔离有一次 HAST 测试过程中温箱通信卡住超时重试机制触发后整个策略执行线程处于等待状态。因为我把超时时间设置得太长默认 30 秒导致一台设备的异常拖延了整个测试流程。排查过程比较曲折最后定位到驱动层的重试逻辑和策略层没有协调。我后来做了两个改进驱动层重试次数、单次超时时间全部通过配置项控制并以日志输出每次重试的上下文。策略层调用任何设备操作前都传入 CancellationToken并且设备操作内部把“总超时”控制在策略层设定的范围内避免无限排队。如果你也在做类似系统强烈建议把设备交互的“超时预算”显式管理起来配置总超时、重试次数、重试间隔三个参数并让它们出现在测试日志里。6.5 单元测试如何覆盖策略模式策略模式的优点之一是方便替换依赖做单测。我在项目里为每个策略类写了一份独立测试用 mock 的温箱驱动和电源驱动替换真实设备。验证 BuildPlanAsync 生成的计划是否包含正确阶段。验证 ExecuteAsync 在某个阶段失败时是否能正确抛出并清理会话。这套测试在不连真实设备的情况下可以跑CI 里自动执行。我建议至少为每个策略覆盖三个场景正常流程、设备超时、参数越界。这是把策略模式真正用好的关键。7. 我在这个项目中的总结体会可靠性测试机上位机这个领域听起来很专本质上却是典型的“算法族不断变化”的软件系统。策略模式在这里不是装饰而是刚需。它帮我解决了三类变化测试流程的变化、设备驱动型号的变化、数据解析格式的变化。如果你正在设计类似的上位机系统我的建议是不要一上来就把所有东西都策略化。刚开始可以把测试执行策略做出来跑通两个流程让团队习惯这个模式。确认有效后再把设备驱动和数据解析逐步迁移过去。这样既控制了风险也让代码库按统一套路演进。另一个经验是策略模式要和依赖注入叠加使用才会真正展现威力。纯手写工厂加 new 对象的策略模式在 .NET Core 项目里只能算半个工程实现。而一旦与容器整合你就拥有了一个可扩展、可测试、可配置的策略框架。最后想分享一个小技巧在每个具体策略类头部用注释写明“这个策略处理哪些测试标准、依赖哪些设备、在什么条件下被选中”。这听起来很土但半年后当你需要加第五种测试类型时你会庆幸这些注释还在。真实的项目里代码能跑只是及格线能让人快速理解才是水平。