我先坦白一个判断:但凡做过半导体测试系统上位机的人,看到“策略模式”四个字,多半会先愣一下——因为这活儿听起来太“业务重、学问深”了,怎么和设计模式扯上关系?
但实际上,我恰恰是在一个可靠性测试机上位机项目里,才真正把策略模式用得服服帖帖的。原因很简单:可靠性测试的场景太杂了。高温工作寿命、高温高湿偏压、温度循环、早期失效筛选,每种测试方法对设备控制、数据采集、判定规则的要求都不一样,而它们又共享同一套上位机框架。如果不用策略模式隔离变化,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; } Task<TestPlan> BuildPlanAsync(TestSpecification spec, CancellationToken ct); Task<TestResult> 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 Task<TestPlan> 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 Task<TestResult> ExecuteAsync(TestPlan plan, ITestContext context, CancellationToken ct) { // 实实在在的执行逻辑:驱动温箱、电源、测量单元 // 并采集关键参数写入上下文 } }HAST 和 TC 策略类结构类似,只是内部调用的设备和流程编排不同。
很多初学者会把具体策略类写得非常臃肿,把设备通信、日志、数据库写入全部堆进去。我在第一版也犯过这个错,后来意识到:策略类不应该关心“怎么给设备发命令”和“怎么存数据”,它更接近一个编舞者(Choreographer),负责编排设备的动作顺序和判断条件。设备通信细节应该下沉到硬件适配层,数据持久化应该走独立的仓储服务。策略类只做流程控制。
2.3 调用方的策略上下文
有了多个策略类之后,调用方(比如测试调度器)不能直接 new 具体策略,需要通过一个工厂或选择器来拿到合适策略。
这里我用了一个简单的策略选择器:
public class ReliabilityTestStrategySelector { private readonly IEnumerable<IReliabilityTestStrategy> _strategies; public ReliabilityTestStrategySelector(IEnumerable<IReliabilityTestStrategy> 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.AddSingleton<HTOLTestStrategy>(); builder.Services.AddSingleton<HASTTestStrategy>(); builder.Services.AddSingleton<TCTTestStrategy>(); builder.Services.AddSingleton<ReliabilityTestStrategySelector>();这里我特意用 Singleton 而不是 Scoped 或 Transient,原因是可靠性测试策略本身是无状态的——它的依赖(设备适配器、日志服务)是单例,策略内部不持有“某一次测试的临时数据”,临时状态都放在 TestContext 对象里传递。如果策略本身持有状态,多线程跑多个测试批次时会互相污染。
有人可能会问:既然策略是无状态的,为什么不直接用 Func<string, 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 Task<TestResult> 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); Task<double> 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; } Task<IReadOnlyList<TestDataPoint>> 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 项目里只能算半个工程实现。而一旦与容器整合,你就拥有了一个可扩展、可测试、可配置的策略框架。
最后想分享一个小技巧:在每个具体策略类头部,用注释写明“这个策略处理哪些测试标准、依赖哪些设备、在什么条件下被选中”。这听起来很土,但半年后当你需要加第五种测试类型时,你会庆幸这些注释还在。真实的项目里,代码能跑只是及格线,能让人快速理解才是水平。