3、软件架构设计原则:分层架构、模块化设计、接口隔离原则、依赖倒置原则在医学设备中的应用

各位同学好,我是老张。今天咱们聊聊软件架构的四大原则,在CT和MRI系统里是怎么落地的。说实话,这些原则听起来像教科书上的老生常谈,但真正在医疗设备里用好了,能救你一条命——我是说,能救你的项目一条命。

3.1 分层架构:别让代码乱成一锅粥

分层架构,说白了就是把软件切成几层。每一层只管自己的事,别越界。我在做第一台CT控制软件时,就吃过这个亏。当时图省事,把硬件驱动和用户界面写在一起,结果换了个探测器型号,整个界面都得重写。嗯,那滋味不好受。

在医学影像设备里,我习惯分这么几层:

  • 硬件抽象层(HAL):管探测器、高压发生器、运动控制这些硬家伙
  • 中间件层:做数据预处理、图像重建、协议转换
  • 应用层:用户界面、扫描协议管理、图像后处理

你想想看,如果哪天MRI的梯度放大器换了供应商,你只需要改HAL层。应用层根本不知道底下换了什么。这就是分层的好处。

核心要点:每一层只依赖它的下一层,绝不跨层调用。比如应用层不能直接写寄存器,必须通过HAL。

3.2 模块化设计:拆开才能修

模块化设计,就是把大系统拆成小模块。每个模块有清晰的输入输出,内部怎么实现别人不管。我在项目中遇到过一件事:有个同事把图像重建算法和运动控制写在一个模块里,结果调试时根本分不清是算法算错了,还是电机没走到位。

后来我们规定:每个模块必须能独立测试。比如:

  • 数据采集模块:只管从探测器拿数据,校验CRC,打包成帧
  • 重建模块:只管把原始数据变成图像,不关心数据从哪来
  • 存储模块:只管把图像存到DICOM格式,不关心图像怎么来的

这样做的好处是,你可以单独升级重建算法,不用动其他模块。我曾经在半夜被叫起来改一个bug,就是因为模块耦合太紧,改一个地方崩了三个模块。从那以后,我对模块化设计有了执念。

我的习惯:每个模块的代码量控制在2000行以内。超过这个数,就该考虑拆分了。

3.3 接口隔离原则:别让客户端依赖它不需要的东西

接口隔离原则,英文叫Interface Segregation Principle,简称ISP。意思是你别设计一个大而全的接口,逼着所有实现类都去实现一堆用不到的方法。

举个例子,在CT系统里,我们有多种探测器:

  • 单排探测器
  • 多排探测器
  • 双源探测器

如果你设计一个IDetector接口,里面放了StartScan()StopScan()GetSliceCount()GetDualEnergyMode()这些方法。那单排探测器就得实现GetDualEnergyMode(),但它根本没有双能模式。这就尴尬了。

我建议的做法是:

// 基础接口
public interface IDetector
{
    void StartScan();
    void StopScan();
}

// 扩展接口
public interface IMultiSliceDetector : IDetector
{
    int GetSliceCount();
}

public interface IDualEnergyDetector : IDetector
{
    bool GetDualEnergyMode();
}

这样,单排探测器只实现IDetector,多排探测器实现IMultiSliceDetector,双源探测器实现IDualEnergyDetector。各取所需,谁也不欠谁。

避坑指南:我曾经见过一个项目,接口里塞了20多个方法,结果每个实现类都有十几个空方法。调试时根本不知道哪个方法该调用哪个。记住:接口越小越好,一个接口最好只做一件事。

3.4 依赖倒置原则:高层模块别依赖低层模块

依赖倒置原则,Dependency Inversion Principle,DIP。名字听着绕口,其实意思很简单:高层模块不应该依赖低层模块,两者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。

在MRI系统里,我们有个典型的场景:图像重建模块需要从数据采集模块拿数据。如果重建模块直接依赖采集模块的具体实现,那哪天采集模块改了数据格式,重建模块也得跟着改。

正确的做法是:

// 定义抽象接口
public interface IDataProvider
{
    RawData GetRawData();
}

// 采集模块实现这个接口
public class AcquisitionModule : IDataProvider
{
    public RawData GetRawData()
    {
        // 从硬件拿数据
    }
}

// 重建模块只依赖接口
public class ReconstructionModule
{
    private IDataProvider _dataProvider;
    
    public ReconstructionModule(IDataProvider provider)
    {
        _dataProvider = provider;
    }
    
    public Image Reconstruct()
    {
        var data = _dataProvider.GetRawData();
        // 做重建
    }
}

你看,重建模块根本不关心数据是从探测器来的,还是从模拟器来的,还是从文件读的。只要实现了IDataProvider接口,就能用。这就是依赖倒置的精髓。

我的经验:在医学设备里,依赖倒置特别适合做单元测试。你可以写一个MockDataProvider,模拟各种异常数据,测试重建模块的健壮性。不用真的去跑硬件,省时省力。

3.5 四个原则怎么配合使用

这四个原则不是孤立的。我习惯这样搭配:

原则 解决的问题 在CT/MRI中的典型应用
分层架构 系统整体结构混乱 硬件驱动、中间件、应用层分离
模块化设计 代码耦合太紧 采集、重建、存储各自独立
接口隔离 接口过于臃肿 不同探测器使用不同接口
依赖倒置 高层依赖低层 重建模块依赖抽象数据接口

举个例子,在CT系统里,我们这样用:

  • 分层架构把系统分成3层
  • 每层内部再拆成模块(模块化)
  • 模块之间通过小接口通信(接口隔离)
  • 高层模块只依赖接口,不依赖具体实现(依赖倒置)

这样一套组合拳下来,系统既好维护,又好扩展。我记得有一次客户要求支持一种新的探测器,我们只花了2天就完成了集成。就是因为架构设计得好,只需要新增一个HAL层的驱动模块,其他层完全不用动。

最后说一句:这些原则不是教条。在项目初期,别过度设计。我一般会在代码出现重复、耦合、难以测试时,才引入这些原则。但一旦引入,就要严格执行。半吊子的架构比没有架构更可怕。