工厂模式在设备驱动中的应用

C# 上位机开发 - 工厂模式在设备驱动中的应用(一篇搞定 建议收藏)

作者:萨达大 | 标签:工厂模式、设备驱动、上位机、配置化

导读:现场常见「同一套软件,A 线 Modbus RTU,B 线 Modbus TCP,C 线厂家私有协议」。用工厂 + 配置 创建 IDeviceDriver,UI 与业务层只依赖接口,换设备不改代码。


一、问题场景:if-else 地狱

不少项目启动类里写着:

csharp 复制代码
if (settings.UseTcp)
    driver = new ModbusTcpDriver(...);
else if (settings.UseRtu)
    driver = new ModbusRtuDriver(...);
else
    driver = new CustomProtocolDriver(...);

窗体、服务、测试里复制粘贴三遍,新增一种驱动就要改 N 处。工厂模式 把「创建哪种驱动」收敛到一个工厂类 + 一份配置


二、统一抽象:IDeviceDriver

与分层架构 01 篇一致,驱动层只关心连接与收发(详见上一篇 HAL)。此处扩展为多种实现:

实现类 适用场景 连接参数
ModbusRtuDriver RS485/RS232 端口名、波特率、校验
ModbusTcpDriver 以太网 PLC IP、端口(通常 502)
CustomProtocolDriver 扫码枪、称重仪等 串口或 TCP + 自定义结束符
csharp 复制代码
public interface IDeviceDriver : IAsyncDisposable
{
    string Name { get; }
    bool IsConnected { get; }
    Task ConnectAsync(CancellationToken ct = default);
    Task DisconnectAsync(CancellationToken ct = default);
    Task<byte[]> TransceiveAsync(byte[] request, int timeoutMs, CancellationToken ct = default);
}

三、ModbusRtuDriver 示例

csharp 复制代码
public sealed class ModbusRtuDriver : IDeviceDriver
{
    private readonly SerialPort _port;
    public string Name => "ModbusRtu";
    public bool IsConnected => _port.IsOpen;

    public ModbusRtuDriver(string portName, int baudRate, Parity parity)
    {
        _port = new SerialPort(portName, baudRate, parity, 8, StopBits.One);
    }

    public Task ConnectAsync(CancellationToken ct = default)
    {
        if (!_port.IsOpen) _port.Open();
        return Task.CompletedTask;
    }

    public Task DisconnectAsync(CancellationToken ct = default)
    {
        if (_port.IsOpen) _port.Close();
        return Task.CompletedTask;
    }

    public async Task<byte[]> TransceiveAsync(byte[] request, int timeoutMs, CancellationToken ct = default)
    {
        _port.Write(request, 0, request.Length);
        var buf = new byte[512];
        var read = await Task.Run(() => _port.Read(buf, 0, buf.Length), ct);
        return buf.AsSpan(0, read).ToArray();
    }

    public async ValueTask DisposeAsync()
    {
        await DisconnectAsync();
        _port.Dispose();
    }
}

四、ModbusTcpDriver 示例

csharp 复制代码
public sealed class ModbusTcpDriver : IDeviceDriver
{
    private readonly string _host;
    private readonly int _port;
    private TcpClient? _client;
    public string Name => "ModbusTcp";
    public bool IsConnected => _client?.Connected == true;

    public ModbusTcpDriver(string host, int port = 502)
    {
        _host = host;
        _port = port;
    }

    public async Task ConnectAsync(CancellationToken ct = default)
    {
        _client = new TcpClient();
        await _client.ConnectAsync(_host, _port, ct);
    }

    public Task DisconnectAsync(CancellationToken ct = default)
    {
        _client?.Close();
        return Task.CompletedTask;
    }

    public async Task<byte[]> TransceiveAsync(byte[] request, int timeoutMs, CancellationToken ct = default)
    {
        if (_client == null) throw new InvalidOperationException("Not connected");
        var stream = _client.GetStream();
        await stream.WriteAsync(request, ct);
        var buf = new byte[512];
        var n = await stream.ReadAsync(buf, ct);
        return buf.AsSpan(0, n).ToArray();
    }

    public async ValueTask DisposeAsync()
    {
        await DisconnectAsync();
        _client?.Dispose();
    }
}

五、CustomProtocolDriver(自定义 ASCII 协议)

例如设备返回 ST,GS,+0012.34kg\r\n

csharp 复制代码
public sealed class CustomProtocolDriver : IDeviceDriver
{
    private readonly SerialPort _port;
    public string Name => "CustomAscii";
    public bool IsConnected => _port.IsOpen;

    public CustomProtocolDriver(string portName, int baudRate)
    {
        _port = new SerialPort(portName, baudRate);
        _port.NewLine = "\r\n";
    }

    public Task ConnectAsync(CancellationToken ct = default)
    {
        if (!_port.IsOpen) _port.Open();
        return Task.CompletedTask;
    }

    public Task DisconnectAsync(CancellationToken ct = default)
    {
        if (_port.IsOpen) _port.Close();
        return Task.CompletedTask;
    }

    public Task<byte[]> TransceiveAsync(byte[] request, int timeoutMs, CancellationToken ct = default)
    {
        _port.Write(request, 0, request.Length);
        var line = _port.ReadLine();
        return Task.FromResult(Encoding.ASCII.GetBytes(line));
    }

    public async ValueTask DisposeAsync()
    {
        await DisconnectAsync();
        _port.Dispose();
    }
}

协议解析仍在协议层;Driver 只保证「发命令、收一行」。


六、DeviceDriverFactory:配置驱动创建

6.1 配置模型

csharp 复制代码
public sealed class DriverConfig
{
    public string DriverType { get; set; } = "ModbusRtu"; // ModbusRtu | ModbusTcp | CustomAscii
    public string? PortName { get; set; }
    public int BaudRate { get; set; } = 9600;
    public string Parity { get; set; } = "Even";
    public string? Host { get; set; }
    public int TcpPort { get; set; } = 502;
}

6.2 JSON 配置示例(devices.json)

json 复制代码
{
  "LineA": {
    "DriverType": "ModbusRtu",
    "PortName": "COM3",
    "BaudRate": 9600,
    "Parity": "Even"
  },
  "LineB": {
    "DriverType": "ModbusTcp",
    "Host": "192.168.1.10",
    "TcpPort": 502
  },
  "Scale1": {
    "DriverType": "CustomAscii",
    "PortName": "COM5",
    "BaudRate": 2400
  }
}

6.3 工厂实现

csharp 复制代码
public static class DeviceDriverFactory
{
    public static IDeviceDriver Create(DriverConfig cfg)
    {
        switch (cfg.DriverType)
        {
            case "ModbusRtu":
                var parity = Enum.Parse<Parity>(cfg.Parity);
                return new ModbusRtuDriver(cfg.PortName!, cfg.BaudRate, parity);
            case "ModbusTcp":
                return new ModbusTcpDriver(cfg.Host!, cfg.TcpPort);
            case "CustomAscii":
                return new CustomProtocolDriver(cfg.PortName!, cfg.BaudRate);
            default:
                throw new NotSupportedException($"Unknown driver: {cfg.DriverType}");
        }
    }

    public static Dictionary<string, IDeviceDriver> CreateAll(IConfiguration configuration)
    {
        var section = configuration.GetSection("Devices");
        var dict = new Dictionary<string, IDeviceDriver>();
        foreach (var child in section.GetChildren())
        {
            var cfg = child.Get<DriverConfig>() ?? new DriverConfig();
            dict[child.Key] = Create(cfg);
        }
        return dict;
    }
}

启动时:

csharp 复制代码
var drivers = DeviceDriverFactory.CreateAll(builder.Configuration);
var lineAProtocol = new ModbusProtocolClient(drivers["LineA"]);
var service = new DeviceService(lineAProtocol);

换 B 线设备 :只改 devices.jsonLineBDriverType 与参数,零 UI 改动


七、与分层架构的配合

#mermaid-svg-wY98tuhVCOL1Ef7M{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wY98tuhVCOL1Ef7M .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wY98tuhVCOL1Ef7M .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wY98tuhVCOL1Ef7M .error-icon{fill:#552222;}#mermaid-svg-wY98tuhVCOL1Ef7M .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wY98tuhVCOL1Ef7M .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wY98tuhVCOL1Ef7M .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wY98tuhVCOL1Ef7M .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wY98tuhVCOL1Ef7M .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wY98tuhVCOL1Ef7M .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wY98tuhVCOL1Ef7M .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wY98tuhVCOL1Ef7M .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wY98tuhVCOL1Ef7M .marker.cross{stroke:#333333;}#mermaid-svg-wY98tuhVCOL1Ef7M svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wY98tuhVCOL1Ef7M p{margin:0;}#mermaid-svg-wY98tuhVCOL1Ef7M .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wY98tuhVCOL1Ef7M .cluster-label text{fill:#333;}#mermaid-svg-wY98tuhVCOL1Ef7M .cluster-label span{color:#333;}#mermaid-svg-wY98tuhVCOL1Ef7M .cluster-label span p{background-color:transparent;}#mermaid-svg-wY98tuhVCOL1Ef7M .label text,#mermaid-svg-wY98tuhVCOL1Ef7M span{fill:#333;color:#333;}#mermaid-svg-wY98tuhVCOL1Ef7M .node rect,#mermaid-svg-wY98tuhVCOL1Ef7M .node circle,#mermaid-svg-wY98tuhVCOL1Ef7M .node ellipse,#mermaid-svg-wY98tuhVCOL1Ef7M .node polygon,#mermaid-svg-wY98tuhVCOL1Ef7M .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wY98tuhVCOL1Ef7M .rough-node .label text,#mermaid-svg-wY98tuhVCOL1Ef7M .node .label text,#mermaid-svg-wY98tuhVCOL1Ef7M .image-shape .label,#mermaid-svg-wY98tuhVCOL1Ef7M .icon-shape .label{text-anchor:middle;}#mermaid-svg-wY98tuhVCOL1Ef7M .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wY98tuhVCOL1Ef7M .rough-node .label,#mermaid-svg-wY98tuhVCOL1Ef7M .node .label,#mermaid-svg-wY98tuhVCOL1Ef7M .image-shape .label,#mermaid-svg-wY98tuhVCOL1Ef7M .icon-shape .label{text-align:center;}#mermaid-svg-wY98tuhVCOL1Ef7M .node.clickable{cursor:pointer;}#mermaid-svg-wY98tuhVCOL1Ef7M .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wY98tuhVCOL1Ef7M .arrowheadPath{fill:#333333;}#mermaid-svg-wY98tuhVCOL1Ef7M .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wY98tuhVCOL1Ef7M .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wY98tuhVCOL1Ef7M .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wY98tuhVCOL1Ef7M .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wY98tuhVCOL1Ef7M .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wY98tuhVCOL1Ef7M .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wY98tuhVCOL1Ef7M .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wY98tuhVCOL1Ef7M .cluster text{fill:#333;}#mermaid-svg-wY98tuhVCOL1Ef7M .cluster span{color:#333;}#mermaid-svg-wY98tuhVCOL1Ef7M div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wY98tuhVCOL1Ef7M .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wY98tuhVCOL1Ef7M rect.text{fill:none;stroke-width:0;}#mermaid-svg-wY98tuhVCOL1Ef7M .icon-shape,#mermaid-svg-wY98tuhVCOL1Ef7M .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wY98tuhVCOL1Ef7M .icon-shape p,#mermaid-svg-wY98tuhVCOL1Ef7M .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wY98tuhVCOL1Ef7M .icon-shape .label rect,#mermaid-svg-wY98tuhVCOL1Ef7M .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wY98tuhVCOL1Ef7M .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wY98tuhVCOL1Ef7M .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wY98tuhVCOL1Ef7M :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} devices.json
DeviceDriverFactory
IDeviceDriver
ModbusProtocolClient
DeviceService
MainForm

工厂只出现在应用启动 / DI 注册 阶段,业务代码不出现 new ModbusRtuDriver


八、依赖注入(可选进阶)

ASP.NET Core 风格注册:

csharp 复制代码
services.AddSingleton<IDeviceDriver>(sp =>
{
    var cfg = sp.GetRequiredService<IConfiguration>().GetSection("Devices:LineA").Get<DriverConfig>()!;
    return DeviceDriverFactory.Create(cfg);
});
services.AddSingleton<IProtocolClient>(sp => new ModbusProtocolClient(sp.GetRequiredService<IDeviceDriver>()));
services.AddSingleton<DeviceService>();

WinForms 可在 Program.cs 里手动组装,原理相同。


九、切换设备实战 checklist

步骤 操作
1 确认新设备物理连接(COM/IP)
2 修改 JSON 中 DriverType 与参数
3 若协议不同,换 IProtocolClient 实现(仍通过工厂或第二个工厂)
4 业务寄存器地址在配置表维护,不改 Form
5 用 Mock Driver 跑一遍回归测试

十、常见问题 FAQ

Q1:工厂和抽象工厂有什么区别?

本文是简单工厂(按字符串创建),上位机足够用。若驱动种类爆炸且族系复杂,可升级为抽象工厂 + 产品族,但维护成本更高。

Q2:多个设备共用一个串口(网关)怎么办?

一个 ModbusRtuDriver 实例,多个 ModbusProtocolClient 共用同一 Driver 时需加互斥锁,或网关 SDK 在 HAL 内排队。

Q3:配置放 JSON 还是数据库?

单机工控机 JSON + 热更新已够用;多产线 MES 下发可用数据库,工厂读表生成 DriverConfig

Q4:DriverType 用字符串还是枚举?

配置文件用字符串可读性好;工厂内 switch 或字典注册 Func<DriverConfig, IDeviceDriver> 避免巨型 switch。

Q5:如何单元测试工厂?

注入临时 JSON,Create 后断言 NameIsConnected 为 false;用 Mock 串口需接口隔离或 ISerialPortWrapper

Q6:和 01 篇 HalSerialPort 重复吗?

HalSerialPort 是教学用 HAL;ModbusRtuDriver 是现场命名。可合并为一个类,只要实现 IDeviceDriver 即可。


十一、小结

  • 所有硬件差异收敛到 IDeviceDriver 实现类
  • DeviceDriverFactory + JSON 实现换线不换代码。
  • UI 只依赖 DeviceService,不依赖具体 Driver。

下一篇介绍轮询数据如何推送到界面------观察者模式与绑定。


上一篇01 上位机分层架构设计

下一篇03 观察者模式与数据绑定

相关推荐
AI人工智能+电脑小能手11 小时前
【大白话说Java面试题 第156题】【06_Spring篇】第16题:Spring 用到了哪些设计模式?
java·spring·设计模式·代理模式·工厂模式
小满Autumn22 天前
log4net 日志框架 — 从配置到实战速查手册
笔记·c#·.net·wpf·上位机·log4net
雪豹阿伟23 天前
21.Winfrom —— 定时器、日期选择器、进度条、表格、DataTable
c#·上位机·winfrom
小满Autumn25 天前
固高GTS运动控制卡 — C#开发完全指南
c#·.net·上位机·运动控制卡
小满Autumn25 天前
雷赛DMC运动控制卡 — C#开发完全指南
c#·.net·上位机·运动控制卡·雷赛
雪豹阿伟1 个月前
18.C# —— 三层结构 + 接口架构实战(UI+Model+DAL+IDAL)
c#·上位机
雪豹阿伟1 个月前
17.C# —— 事件
c#·上位机
小满Autumn1 个月前
MVVM Light 架构笔记:定位器、命令、消息与 IoC 实践
笔记·学习·架构·c#·上位机·mvvm
雪豹阿伟1 个月前
16.C# —— 委托,委托实例,多播委托,内置委托,泛型委托
c#·上位机