第三章 常见架构模式:分层架构、微服务、事件驱动与管道-过滤器

聊到工业控制软件的架构,我脑子里第一个蹦出来的词就是「模式」。说白了,架构模式就是前人踩过无数坑之后,总结出来的一套「最佳实践」。今天咱们就掰开揉碎,聊聊四种最常见的:分层架构、微服务架构、事件驱动架构,还有管道-过滤器架构。

嗯,这四种模式我都在实际项目中用过。有的让我省心,有的让我头疼。咱们一个一个说。

3.1 分层架构(Layered Architecture)

这是最经典的模式,没有之一。你想想看,一个工控系统,从最底层的传感器数据采集,到上层的HMI显示,中间要经过多少层处理?分层架构就是把这些处理步骤,按职责拆成不同的层。

典型的工控分层长这样:

层级 职责 常见组件
表示层 人机交互、数据可视化 HMI、Web界面、移动端
业务逻辑层 控制算法、规则引擎、报警处理 PLC逻辑、状态机、计算模块
数据访问层 数据持久化、历史存储 实时数据库、关系数据库、OPC UA
硬件抽象层 屏蔽硬件差异、统一接口 驱动层、I/O映射、协议转换

我个人习惯把每一层都当成一个「黑盒子」。上层只管调用下层的接口,不用管下层怎么实现的。这样做的好处很明显——哪一层出了问题,就只修哪一层,不会牵一发动全身。

核心原则:每一层只依赖它的直接下层。千万别跨层调用!

我在项目中遇到过一件事:有个同事为了图方便,直接在表示层里调用了硬件抽象层的函数。结果后来换了另一家的PLC,表示层代码全得重写。你说冤不冤?

我的建议:分层架构适合大多数工控项目,尤其是那些需求相对稳定、团队规模不大的场景。但要注意,层数别太多。我见过有人分了七八层,结果每次改需求都要改四五层代码,调试起来想死的心都有。

3.2 微服务架构(Microservices)

微服务这几年特别火。说白了,就是把一个大的单体应用,拆成一堆小的、独立运行的服务。每个服务都有自己的数据库,服务之间通过轻量级的通信机制(比如REST API、消息队列)来协作。

在工控领域,微服务架构特别适合那些功能模块边界清晰、需要独立扩展的场景。比如:

  • 报警服务:独立处理报警产生、确认、历史查询
  • 报表服务:专门负责生成日报、月报、趋势图
  • 配方管理服务:管理不同产品的工艺参数
  • 设备管理服务:跟踪设备状态、维护计划

每个服务都可以独立开发、独立部署、独立扩展。你想想看,如果报警量突然暴增,你只需要给报警服务多分配几个实例就行,其他服务完全不受影响。

注意:微服务不是银弹。我曾经在一个小项目里强行上微服务,结果光服务间通信的代码就写了一堆,最后维护成本比单体架构还高。小项目、小团队,别盲目追微服务。

微服务架构的另一个坑是数据一致性。工控系统对实时性要求很高,如果两个服务之间的数据不同步,那麻烦就大了。我建议用事件驱动的方式来解耦服务间的数据同步,这个咱们下一节细说。

3.3 事件驱动架构(Event-Driven Architecture)

事件驱动架构,说白了就是「你只管发事件,谁关心谁处理」。系统里有一个事件总线(Event Bus),各个模块只管往总线上发事件,或者从总线上订阅自己感兴趣的事件。

举个例子:

// 事件定义
public class TemperatureAlarmEvent {
    public string SensorId { get; set; }
    public double Temperature { get; set; }
    public DateTime Timestamp { get; set; }
}

// 事件发布者
public class TemperatureSensor {
    public void OnTemperatureChanged(double value) {
        if (value > 100) {
            EventBus.Publish(new TemperatureAlarmEvent {
                SensorId = this.Id,
                Temperature = value,
                Timestamp = DateTime.Now
            });
        }
    }
}

// 事件订阅者
public class AlarmLogger {
    public AlarmLogger() {
        EventBus.Subscribe<TemperatureAlarmEvent>(OnTemperatureAlarm);
    }
    
    private void OnTemperatureAlarm(TemperatureAlarmEvent e) {
        // 记录报警到数据库
        Database.Save(e);
    }
}

这样做的好处是模块之间完全解耦。传感器模块不需要知道谁在处理报警,报警模块也不需要知道传感器是怎么采集数据的。你想想看,如果以后要加一个短信报警功能,只需要新写一个订阅者就行,完全不用改现有代码。

关键点:事件驱动架构的核心是「异步」和「非阻塞」。发布者发完事件就继续干自己的事,不用等订阅者处理完。这在工控系统里特别重要——你不能因为一个报警处理慢了,就影响整个控制循环。

嗯,这里要注意一个坑:事件风暴。如果事件太多,或者事件处理逻辑太复杂,系统可能会被事件淹没。我建议给事件设置优先级,关键事件(比如紧急停机)走高速通道,普通事件(比如日志记录)走普通通道。

3.4 管道-过滤器架构(Pipes and Filters)

这个模式在工控领域用得特别多,尤其是数据处理场景。你想想看,一个信号从传感器采集进来,要经过滤波、归一化、特征提取、阈值判断……这一系列步骤,就像流水线一样。管道-过滤器架构就是干这个的。

每个处理步骤是一个「过滤器」,过滤器之间通过「管道」连接。数据从管道流入,经过过滤器处理,再流入下一个管道。

看个简单的例子:

// 过滤器接口
public interface IFilter<TInput, TOutput> {
    TOutput Process(TInput input);
}

// 具体过滤器:低通滤波
public class LowPassFilter : IFilter<double[], double[]> {
    private double _cutoffFrequency;
    
    public LowPassFilter(double cutoff) {
        _cutoffFrequency = cutoff;
    }
    
    public double[] Process(double[] signal) {
        // 实现低通滤波算法
        // ...
        return filteredSignal;
    }
}

// 管道:连接过滤器
public class Pipeline<T> {
    private List<IFilter<T, T>> _filters = new List<IFilter<T, T>>();
    
    public Pipeline<T> AddFilter(IFilter<T, T> filter) {
        _filters.Add(filter);
        return this;
    }
    
    public T Execute(T input) {
        T result = input;
        foreach (var filter in _filters) {
            result = filter.Process(result);
        }
        return result;
    }
}

// 使用
var pipeline = new Pipeline<double[]>()
    .AddFilter(new LowPassFilter(50))
    .AddFilter(new NormalizeFilter())
    .AddFilter(new FeatureExtractor());
    
var processedSignal = pipeline.Execute(rawSignal);

这种模式的好处是每个过滤器都是独立的,可以单独测试、单独替换。我在项目中遇到过需要更换滤波算法的情况,只需要换掉对应的过滤器就行,其他代码完全不用动。

避坑指南:我曾经在一个项目里把过滤器链拉得太长,结果数据经过十几个过滤器之后,延迟变得不可接受。后来我用了并行管道——把那些互不依赖的过滤器放到不同的线程里跑,才把延迟降下来。记住,管道-过滤器架构虽然灵活,但要注意性能瓶颈。

四种模式各有千秋。我个人的经验是:

  • 分层架构:适合大多数工控项目,尤其是中小规模
  • 微服务架构:适合大型、多团队协作、需要独立扩展的项目
  • 事件驱动架构:适合模块间解耦、异步处理的场景
  • 管道-过滤器架构:适合数据处理流水线、信号处理等场景

实际项目中,这些模式往往不是非此即彼的。我经常在一个系统里同时用两三种模式。比如,整体用分层架构,但层与层之间用事件驱动来通信,而数据处理模块内部用管道-过滤器。关键是要理解每种模式的优缺点,然后根据实际情况灵活组合。

嗯,今天就聊到这儿。下一章咱们聊聊架构设计中的「非功能性需求」,比如性能、可靠性、安全性这些。这些东西看着虚,但真到了项目里,都是要命的事。